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:
2026-04-25 21:52:33 +02:00
parent 14cd888c2e
commit 643d3a2abf
3 changed files with 289 additions and 0 deletions
+191
View File
@@ -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();
}
+32
View File
@@ -0,0 +1,32 @@
#pragma once
// Squarified treemap (Bruls, Huijbrechts, van Wijk 2000).
//
// Layout puro (treemap_layout) separado del render (treemap) — la layout
// no toca ImGui y es testeable.
#include "imgui.h"
#include <string>
#include <vector>
struct TreemapItem {
std::string label;
float value;
ImU32 color;
};
struct TreemapRect {
ImVec2 min;
ImVec2 max;
const TreemapItem* item;
};
// Layout puro. Devuelve un rect por item con coords absolutas dentro de [0,0]-region.
// Items con value <= 0 se ignoran.
std::vector<TreemapRect> treemap_layout(const std::vector<TreemapItem>& items,
ImVec2 region);
// Render. Si size.x <= 0 usa el ancho disponible. Reserva size en el layout ImGui.
void treemap(const char* id,
const std::vector<TreemapItem>& items,
ImVec2 size = ImVec2(-1.0f, 300.0f));
+66
View File
@@ -0,0 +1,66 @@
---
name: treemap
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "void treemap(const char* id, const std::vector<TreemapItem>& items, ImVec2 size)"
description: "Squarified treemap (Bruls, Huijbrechts, van Wijk) para jerarquias planas con valores. Layout puro separado del render."
tags: [imgui, drawlist, chart, visualization, treemap, hierarchy, squarified]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/treemap.cpp"
framework: imgui
params:
- name: id
desc: "Identificador unico para PushID (evita colisiones entre treemaps)"
- name: items
desc: "Vector de TreemapItem {label, value, color}. Items con value <= 0 se ignoran"
- name: size
desc: "Tamano del rect del treemap. x <= 0 usa el ancho disponible"
output: "Renderiza el treemap en el frame ImGui actual usando AddRectFilled + AddText sobre el WindowDrawList"
---
# treemap
Treemap squarified: dado un vector de items con valor numerico, divide el rect dado en cells cuya area es proporcional al valor del item. El algoritmo de Bruls et al. minimiza el aspect ratio (cells lo mas cuadradas posibles).
## API
```cpp
struct TreemapItem { std::string label; float value; ImU32 color; };
struct TreemapRect { ImVec2 min, max; const TreemapItem* item; };
std::vector<TreemapRect> treemap_layout(const std::vector<TreemapItem>&, ImVec2 region); // pure
void treemap(const char* id, const std::vector<TreemapItem>&, ImVec2 size = {-1, 300});
```
`treemap_layout` es pura — devuelve rects en coords [0..region]. `treemap` invoca el layout y renderiza con `AddRectFilled` + label + valor cuando caben.
## Conservacion del area
La suma de areas de los rects es igual al area de la region (modulo errores de redondeo). Util para tests.
## Limitaciones MVP
- Solo jerarquia plana (no recursivo). Para jerarquias anidadas, llamar `treemap_layout` recursivamente sobre cada cell.
- Sin interaccion (click, zoom).
## Ejemplo
```cpp
std::vector<TreemapItem> items = {
{"vivienda", 950, IM_COL32(180,120,200,255)},
{"comida", 320, IM_COL32(120,180,200,255)},
{"transporte", 180, IM_COL32(200,180,120,255)},
};
treemap("##gastos", items, ImVec2(-1, 300));
```