diff --git a/cpp/functions/viz/treemap.cpp b/cpp/functions/viz/treemap.cpp new file mode 100644 index 00000000..e63466d0 --- /dev/null +++ b/cpp/functions/viz/treemap.cpp @@ -0,0 +1,191 @@ +#include "viz/treemap.h" + +#include +#include +#include + +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& 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& row, + const std::vector& row_idx, + ImVec2 rect_min, + ImVec2 rect_max, + std::vector& 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 treemap_layout(const std::vector& items, + ImVec2 region) { + std::vector 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 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 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 row; + std::vector 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 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& 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(); +} diff --git a/cpp/functions/viz/treemap.h b/cpp/functions/viz/treemap.h new file mode 100644 index 00000000..4f6604e2 --- /dev/null +++ b/cpp/functions/viz/treemap.h @@ -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 +#include + +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 treemap_layout(const std::vector& 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& items, + ImVec2 size = ImVec2(-1.0f, 300.0f)); diff --git a/cpp/functions/viz/treemap.md b/cpp/functions/viz/treemap.md new file mode 100644 index 00000000..4cb4a249 --- /dev/null +++ b/cpp/functions/viz/treemap.md @@ -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& 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 treemap_layout(const std::vector&, ImVec2 region); // pure +void treemap(const char* id, const std::vector&, 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 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)); +```