0487180ac2
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.
192 lines
6.3 KiB
C++
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();
|
|
}
|