Files
egutierrez 0487180ac2 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.
2026-04-25 21:52:33 +02:00

192 lines
6.3 KiB
C++

#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();
}