feat(viz): treemap squarified (Bruls et al.) — layout puro + render DrawList
treemap_layout devuelve TreemapRect{min, max, item} con coords absolutas
dentro de la region. La suma de areas == area total (verificado via test
standalone, ratio=1.000000). El render usa AddRectFilled + AddText cuando
labels y valores caben dentro de la cell.
Limitaciones MVP: jerarquia plana (no recursivo), sin interaccion.
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
#include "viz/treemap.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
namespace {
|
||||
|
||||
// Worst aspect ratio en la fila si añadimos `next` (Bruls et al.).
|
||||
// row: vector de areas (mismo orden que items), w: lado corto del rect actual.
|
||||
float worst_ratio(const std::vector<float>& row, float w) {
|
||||
if (row.empty() || w <= 0.0f) return 1e30f;
|
||||
float sum = 0.0f, mn = row[0], mx = row[0];
|
||||
for (float v : row) {
|
||||
sum += v;
|
||||
if (v < mn) mn = v;
|
||||
if (v > mx) mx = v;
|
||||
}
|
||||
if (sum <= 0.0f) return 1e30f;
|
||||
float w2 = w * w;
|
||||
float s2 = sum * sum;
|
||||
float r1 = (w2 * mx) / s2;
|
||||
float r2 = s2 / (w2 * mn);
|
||||
return std::max(r1, r2);
|
||||
}
|
||||
|
||||
// Coloca una fila en el rect actual, devuelve el rect remanente.
|
||||
// Si el rect es mas ancho que alto, la fila ocupa el lado izquierdo (vertical strip).
|
||||
struct RowOut {
|
||||
ImVec2 next_min;
|
||||
ImVec2 next_max;
|
||||
};
|
||||
|
||||
RowOut place_row(const std::vector<float>& row,
|
||||
const std::vector<int>& row_idx,
|
||||
ImVec2 rect_min,
|
||||
ImVec2 rect_max,
|
||||
std::vector<TreemapRect>& out) {
|
||||
float W = rect_max.x - rect_min.x;
|
||||
float H = rect_max.y - rect_min.y;
|
||||
float sum = 0.0f;
|
||||
for (float v : row) sum += v;
|
||||
if (sum <= 0.0f) return {rect_min, rect_max};
|
||||
|
||||
bool horizontal_strip = (W >= H); // strip ocupa lado izquierdo (vertical column de cells horizontales)
|
||||
if (horizontal_strip) {
|
||||
float strip_w = sum / H;
|
||||
float y = rect_min.y;
|
||||
for (size_t i = 0; i < row.size(); i++) {
|
||||
float h = row[i] / strip_w;
|
||||
TreemapRect r;
|
||||
r.min = ImVec2(rect_min.x, y);
|
||||
r.max = ImVec2(rect_min.x + strip_w, std::min(y + h, rect_max.y));
|
||||
r.item = nullptr;
|
||||
out[row_idx[i]] = r;
|
||||
y += h;
|
||||
}
|
||||
return {ImVec2(rect_min.x + strip_w, rect_min.y), rect_max};
|
||||
} else {
|
||||
float strip_h = sum / W;
|
||||
float x = rect_min.x;
|
||||
for (size_t i = 0; i < row.size(); i++) {
|
||||
float w = row[i] / strip_h;
|
||||
TreemapRect r;
|
||||
r.min = ImVec2(x, rect_min.y);
|
||||
r.max = ImVec2(std::min(x + w, rect_max.x), rect_min.y + strip_h);
|
||||
r.item = nullptr;
|
||||
out[row_idx[i]] = r;
|
||||
x += w;
|
||||
}
|
||||
return {rect_min, ImVec2(rect_max.x, rect_min.y + strip_h)};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<TreemapRect> treemap_layout(const std::vector<TreemapItem>& items,
|
||||
ImVec2 region) {
|
||||
std::vector<TreemapRect> out(items.size(), {ImVec2(0,0), ImVec2(0,0), nullptr});
|
||||
if (items.empty() || region.x <= 0.0f || region.y <= 0.0f) return out;
|
||||
|
||||
// Filtra y ordena indices por value desc.
|
||||
std::vector<int> idx;
|
||||
idx.reserve(items.size());
|
||||
float total = 0.0f;
|
||||
for (size_t i = 0; i < items.size(); i++) {
|
||||
if (items[i].value > 0.0f) {
|
||||
idx.push_back((int)i);
|
||||
total += items[i].value;
|
||||
}
|
||||
}
|
||||
if (idx.empty() || total <= 0.0f) return out;
|
||||
|
||||
std::sort(idx.begin(), idx.end(),
|
||||
[&](int a, int b) { return items[a].value > items[b].value; });
|
||||
|
||||
// Areas escaladas al area total del rect.
|
||||
float region_area = region.x * region.y;
|
||||
float scale = region_area / total;
|
||||
std::vector<float> areas(idx.size());
|
||||
for (size_t i = 0; i < idx.size(); i++) areas[i] = items[idx[i]].value * scale;
|
||||
|
||||
ImVec2 rmin(0.0f, 0.0f);
|
||||
ImVec2 rmax = region;
|
||||
|
||||
std::vector<float> row;
|
||||
std::vector<int> row_indices; // indices en `out` (= idx[i])
|
||||
size_t cursor = 0;
|
||||
|
||||
while (cursor < idx.size()) {
|
||||
float w = std::min(rmax.x - rmin.x, rmax.y - rmin.y);
|
||||
if (w <= 0.0f) break;
|
||||
|
||||
std::vector<float> row_try = row;
|
||||
row_try.push_back(areas[cursor]);
|
||||
float wr_now = worst_ratio(row, w);
|
||||
float wr_next = worst_ratio(row_try, w);
|
||||
|
||||
if (row.empty() || wr_next <= wr_now) {
|
||||
row = row_try;
|
||||
row_indices.push_back(idx[cursor]);
|
||||
cursor++;
|
||||
} else {
|
||||
RowOut ro = place_row(row, row_indices, rmin, rmax, out);
|
||||
rmin = ro.next_min;
|
||||
rmax = ro.next_max;
|
||||
row.clear();
|
||||
row_indices.clear();
|
||||
}
|
||||
}
|
||||
if (!row.empty()) {
|
||||
place_row(row, row_indices, rmin, rmax, out);
|
||||
}
|
||||
|
||||
// Asocia el item.
|
||||
for (size_t i = 0; i < items.size(); i++) {
|
||||
out[i].item = &items[i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void treemap(const char* id,
|
||||
const std::vector<TreemapItem>& items,
|
||||
ImVec2 size) {
|
||||
ImGui::PushID(id);
|
||||
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
float w = (size.x > 0.0f) ? size.x : avail.x;
|
||||
float h = (size.y > 0.0f) ? size.y : 200.0f;
|
||||
|
||||
ImVec2 origin = ImGui::GetCursorScreenPos();
|
||||
ImGui::Dummy(ImVec2(w, h));
|
||||
|
||||
auto rects = treemap_layout(items, ImVec2(w, h));
|
||||
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
|
||||
// Borde exterior tenue
|
||||
dl->AddRect(origin, ImVec2(origin.x + w, origin.y + h),
|
||||
IM_COL32(80, 80, 90, 200), 0.0f, 0, 1.0f);
|
||||
|
||||
for (const auto& r : rects) {
|
||||
if (!r.item) continue;
|
||||
ImVec2 a(origin.x + r.min.x, origin.y + r.min.y);
|
||||
ImVec2 b(origin.x + r.max.x, origin.y + r.max.y);
|
||||
if (b.x - a.x < 1.0f || b.y - a.y < 1.0f) continue;
|
||||
|
||||
dl->AddRectFilled(a, b, r.item->color);
|
||||
dl->AddRect(a, b, IM_COL32(20, 22, 28, 220), 0.0f, 0, 1.0f);
|
||||
|
||||
// Label si cabe
|
||||
const char* lbl = r.item->label.c_str();
|
||||
ImVec2 ts = ImGui::CalcTextSize(lbl);
|
||||
float cell_w = b.x - a.x;
|
||||
float cell_h = b.y - a.y;
|
||||
if (ts.x + 6.0f <= cell_w && ts.y + 4.0f <= cell_h) {
|
||||
dl->AddText(ImVec2(a.x + 4.0f, a.y + 3.0f),
|
||||
IM_COL32(245, 246, 250, 240), lbl);
|
||||
// valor en segunda linea si tambien cabe
|
||||
char buf[32];
|
||||
std::snprintf(buf, sizeof(buf), "%.0f", r.item->value);
|
||||
ImVec2 vs = ImGui::CalcTextSize(buf);
|
||||
if (vs.x + 6.0f <= cell_w && ts.y + vs.y + 6.0f <= cell_h) {
|
||||
dl->AddText(ImVec2(a.x + 4.0f, a.y + 3.0f + ts.y + 1.0f),
|
||||
IM_COL32(220, 222, 235, 200), buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
Reference in New Issue
Block a user