merge: issue/0034 — scientific viz (treemap, sankey, chord, contour, voronoi)

# Conflicts:
#	cpp/apps/primitives_gallery/CMakeLists.txt
#	cpp/apps/primitives_gallery/demos.h
#	cpp/apps/primitives_gallery/main.cpp
This commit is contained in:
2026-04-25 21:55:49 +02:00
20 changed files with 1490 additions and 0 deletions
@@ -16,6 +16,7 @@ add_imgui_app(primitives_gallery
${CMAKE_SOURCE_DIR}/functions/core/bezier_editor.cpp
${CMAKE_SOURCE_DIR}/functions/core/timeline.cpp
demos_sql.cpp
demos_scientific.cpp
# text_editor + file_watcher (issue 0025)
${CMAKE_SOURCE_DIR}/functions/core/text_editor.cpp
${CMAKE_SOURCE_DIR}/functions/core/file_watcher.cpp
@@ -52,6 +53,12 @@ add_imgui_app(primitives_gallery
# 3D viz primitives (issue 0028, ImPlot3D)
${CMAKE_SOURCE_DIR}/functions/viz/surface_plot_3d.cpp
${CMAKE_SOURCE_DIR}/functions/viz/scatter_3d.cpp
# Scientific viz (issue 0034)
${CMAKE_SOURCE_DIR}/functions/viz/treemap.cpp
${CMAKE_SOURCE_DIR}/functions/viz/sankey.cpp
${CMAKE_SOURCE_DIR}/functions/viz/chord.cpp
${CMAKE_SOURCE_DIR}/functions/viz/contour.cpp
${CMAKE_SOURCE_DIR}/functions/viz/voronoi.cpp
# Graph stack (instanced GPU + Barnes-Hut + spatial hash)
${CMAKE_SOURCE_DIR}/functions/viz/graph_types.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_renderer.cpp
+5
View File
@@ -41,6 +41,11 @@ void demo_table_view();
void demo_surface_plot_3d(); // issue 0028, ImPlot3D
void demo_scatter_3d(); // issue 0028, ImPlot3D
void demo_mesh_viewer(); // issue 0029
void demo_treemap(); // issue 0034
void demo_sankey(); // issue 0034
void demo_chord(); // issue 0034
void demo_contour(); // issue 0034
void demo_voronoi(); // issue 0034
// --- Gfx ---
void demo_shader_canvas();
@@ -0,0 +1,208 @@
// demos_scientific.cpp — demos para los 5 charts cientificos del issue 0034:
// treemap, sankey, chord, contour, voronoi.
#include "demos.h"
#include "demo.h"
#include "viz/treemap.h"
#include "viz/sankey.h"
#include "viz/chord.h"
#include "viz/contour.h"
#include "viz/voronoi.h"
#include <imgui.h>
#include <cmath>
#include <cstdlib>
#include <vector>
namespace gallery {
// ---------------------------------------------------------------------------
// treemap
// ---------------------------------------------------------------------------
void demo_treemap() {
demo_header("treemap", "v1.0.0",
"Squarified treemap (Bruls et al.) para jerarquias planas con valores. "
"Algoritmo puro separado del render.");
section("Gastos por categoria");
{
std::vector<TreemapItem> items = {
{"vivienda", 950.0f, IM_COL32(180, 120, 200, 255)},
{"comida", 320.0f, IM_COL32(120, 180, 200, 255)},
{"transporte", 180.0f, IM_COL32(200, 180, 120, 255)},
{"ocio", 140.0f, IM_COL32(200, 120, 160, 255)},
{"salud", 90.0f, IM_COL32(120, 200, 160, 255)},
{"otros", 60.0f, IM_COL32(160, 160, 200, 255)},
};
treemap("##gastos", items, ImVec2(-1, 320));
}
code_block(
"std::vector<TreemapItem> items = {\n"
" {\"vivienda\", 950.0f, IM_COL32(180,120,200,255)},\n"
" {\"comida\", 320.0f, IM_COL32(120,180,200,255)},\n"
" ...\n"
"};\n"
"treemap(\"##gastos\", items, ImVec2(-1, 320));"
);
}
// ---------------------------------------------------------------------------
// sankey
// ---------------------------------------------------------------------------
void demo_sankey() {
demo_header("sankey", "v1.0.0",
"Sankey diagram para flujos source -> target. BFS topologico para columnas, "
"bandas curvas (bezier cubico) para los links. Asume DAG.");
section("Clientes -> productos -> categorias");
{
std::vector<SankeyNode> nodes = {
{"premium"}, {"basicos"},
{"laptops"}, {"phones"}, {"tablets"},
{"hardware"}, {"software"}, {"servicios"},
};
std::vector<SankeyLink> links = {
// clientes -> productos
{0, 2, 80}, {0, 3, 30}, {0, 4, 15},
{1, 3, 60}, {1, 4, 40}, {1, 2, 20},
// productos -> categorias
{2, 5, 70}, {2, 6, 30},
{3, 5, 50}, {3, 7, 40},
{4, 6, 35}, {4, 7, 20},
};
sankey("##flow", nodes, links, ImVec2(-1, 360));
}
code_block(
"std::vector<SankeyNode> nodes = {{\"premium\"}, {\"basicos\"}, ...};\n"
"std::vector<SankeyLink> links = {{0, 2, 80}, {0, 3, 30}, ...};\n"
"sankey(\"##flow\", nodes, links, ImVec2(-1, 360));"
);
}
// ---------------------------------------------------------------------------
// chord
// ---------------------------------------------------------------------------
void demo_chord() {
demo_header("chord", "v1.0.0",
"Chord diagram para matrices NxN. Arcos proporcionales a sum(row) + cuerdas "
"internas con bezier cubico.");
section("Flujos entre paises (matriz 6x6 simetrica)");
{
constexpr int N = 6;
// simetrica de "comercio" entre 6 paises
static float M[N * N] = {
0, 10, 6, 12, 4, 3,
10, 0, 14, 3, 8, 2,
6, 14, 0, 9, 11, 5,
12, 3, 9, 0, 7, 6,
4, 8, 11, 7, 0, 13,
3, 2, 5, 6, 13, 0,
};
static const char* labels[N] = {"ESP", "FRA", "ITA", "DEU", "PRT", "GBR"};
chord("##chord", M, N, labels, ImVec2(420, 420));
}
code_block(
"float M[N*N] = { // simetrica\n"
" 0, 10, 6, 12, 4, 3,\n"
" 10, 0, 14, 3, 8, 2,\n"
" ...\n"
"};\n"
"const char* labels[6] = {\"ESP\",\"FRA\",\"ITA\",\"DEU\",\"PRT\",\"GBR\"};\n"
"chord(\"##c\", M, 6, labels);"
);
}
// ---------------------------------------------------------------------------
// contour
// ---------------------------------------------------------------------------
void demo_contour() {
demo_header("contour", "v1.0.0",
"Contour plot 2D via marching squares. Para una gaussiana centrada los "
"contornos resultantes son aproximadamente concentricos.");
constexpr int N = 32;
static float grid[N * N];
static bool init = false;
if (!init) {
// Mezcla de 2 gaussianas (peak central + secundario)
for (int y = 0; y < N; y++) {
for (int x = 0; x < N; x++) {
float dx1 = x - N * 0.45f, dy1 = y - N * 0.5f;
float dx2 = x - N * 0.75f, dy2 = y - N * 0.3f;
float v = std::exp(-(dx1 * dx1 + dy1 * dy1) / 70.0f)
+ 0.55f * std::exp(-(dx2 * dx2 + dy2 * dy2) / 30.0f);
grid[y * N + x] = v;
}
}
init = true;
}
static const float levels[] = {0.15f, 0.30f, 0.50f, 0.70f, 0.90f};
contour("##gauss", grid, N, N, levels, 5, ImVec2(-1, 320));
code_block(
"constexpr int N = 32;\n"
"float grid[N*N];\n"
"for (int y = 0; y < N; y++)\n"
" for (int x = 0; x < N; x++) {\n"
" float dx = x - N/2.0f, dy = y - N/2.0f;\n"
" grid[y*N + x] = std::exp(-(dx*dx + dy*dy) / 80.0f);\n"
" }\n"
"float levels[] = {0.15f, 0.30f, 0.50f, 0.70f, 0.90f};\n"
"contour(\"##gauss\", grid, N, N, levels, 5);"
);
}
// ---------------------------------------------------------------------------
// voronoi
// ---------------------------------------------------------------------------
void demo_voronoi() {
demo_header("voronoi", "v1.0.0",
"Diagrama de Voronoi via raster brute-force (MVP). Tiles 4x4 px coloreados "
"por el seed mas cercano. Suficiente para N <= 200.");
constexpr int N = 30;
static ImVec2 seeds [N];
static ImU32 colors[N];
static bool init = false;
if (!init) {
unsigned seed = 7;
auto rnd = [&]() {
seed = seed * 1103515245u + 12345u;
return (float)((seed >> 16) & 0x7fff) / 32768.0f;
};
// El render escala automaticamente; las posiciones se asumen en coords del rect.
// Como no sabemos W/H aqui, usamos coords aproximadas para 600x300 y el clip
// dentro de voronoi se encarga de mantenerlas en rango.
for (int i = 0; i < N; i++) {
seeds [i] = ImVec2(rnd() * 600.0f, rnd() * 300.0f);
colors[i] = IM_COL32(40 + (int)(rnd() * 200),
40 + (int)(rnd() * 200),
60 + (int)(rnd() * 195),
230);
}
init = true;
}
voronoi("##v", seeds, N, colors, ImVec2(-1, 300));
code_block(
"ImVec2 seeds[30];\n"
"ImU32 colors[30];\n"
"for (int i = 0; i < 30; i++) {\n"
" seeds [i] = ImVec2(rnd() * 600.0f, rnd() * 300.0f);\n"
" colors[i] = IM_COL32(rnd_byte(), rnd_byte(), rnd_byte(), 230);\n"
"}\n"
"voronoi(\"##v\", seeds, 30, colors, ImVec2(-1, 300));"
);
}
} // namespace gallery
+5
View File
@@ -68,6 +68,11 @@ static const DemoEntry k_demos[] = {
{"surface_plot_3d", "surface_plot_3d", "Viz", &gallery::demo_surface_plot_3d},
{"scatter_3d", "scatter_3d", "Viz", &gallery::demo_scatter_3d},
{"mesh_viewer", "mesh_viewer", "Viz", &gallery::demo_mesh_viewer},
{"treemap", "treemap", "Viz", &gallery::demo_treemap},
{"sankey", "sankey", "Viz", &gallery::demo_sankey},
{"chord", "chord", "Viz", &gallery::demo_chord},
{"contour", "contour", "Viz", &gallery::demo_contour},
{"voronoi", "voronoi", "Viz", &gallery::demo_voronoi},
// Gfx (shaders_lab core)
{"shader_canvas", "shader_canvas", "Gfx", &gallery::demo_shader_canvas},
{"gl_texture", "gl_texture_load", "Gfx", &gallery::demo_gl_texture}, // wave 1
+200
View File
@@ -0,0 +1,200 @@
#include "viz/chord.h"
#include <cmath>
#include <vector>
namespace {
ImU32 chord_palette(int idx) {
static const ImU32 P[] = {
IM_COL32(120, 144, 252, 230),
IM_COL32( 92, 200, 200, 230),
IM_COL32(250, 176, 92, 230),
IM_COL32(180, 120, 200, 230),
IM_COL32( 92, 200, 130, 230),
IM_COL32(250, 120, 130, 230),
IM_COL32(180, 200, 92, 230),
IM_COL32(120, 200, 230, 230),
};
constexpr int N = sizeof(P) / sizeof(P[0]);
return P[((idx % N) + N) % N];
}
} // namespace
void chord(const char* id,
const float* matrix,
int n,
const char* const* labels,
ImVec2 size) {
ImGui::PushID(id);
ImVec2 avail = ImGui::GetContentRegionAvail();
float W = (size.x > 0.0f) ? size.x : (avail.x > 0.0f ? avail.x : 400.0f);
float H = (size.y > 0.0f) ? size.y : 400.0f;
ImVec2 origin = ImGui::GetCursorScreenPos();
ImGui::Dummy(ImVec2(W, H));
if (n <= 0 || matrix == nullptr) { ImGui::PopID(); return; }
ImVec2 center(origin.x + W * 0.5f, origin.y + H * 0.5f);
float r_outer = 0.5f * std::min(W, H) - 30.0f;
float r_inner = r_outer - 12.0f;
if (r_inner < 10.0f) { ImGui::PopID(); return; }
// Sumas por nodo
std::vector<float> totals(n, 0.0f);
float grand = 0.0f;
for (int i = 0; i < n; i++) {
float s = 0.0f;
for (int j = 0; j < n; j++) s += matrix[i * n + j];
totals[i] = s;
grand += s;
}
if (grand <= 0.0f) { ImGui::PopID(); return; }
constexpr float kTAU = 6.28318530717958647692f;
const float gap = 0.02f; // radianes entre arcos
// angulos start/end por nodo
std::vector<float> a_start(n), a_end(n);
{
float total_gap = gap * (float)n;
float available = kTAU - total_gap;
float a = -kTAU * 0.25f; // arrancar arriba
for (int i = 0; i < n; i++) {
float sweep = (totals[i] / grand) * available;
a_start[i] = a;
a_end[i] = a + sweep;
a = a_end[i] + gap;
}
}
ImDrawList* dl = ImGui::GetWindowDrawList();
// Render arcos exteriores (gruesos)
for (int i = 0; i < n; i++) {
if (a_end[i] - a_start[i] < 1e-4f) continue;
const int STEPS = std::max(8, (int)((a_end[i] - a_start[i]) * 64.0f));
ImU32 col = chord_palette(i);
// Polygon entre r_inner y r_outer
std::vector<ImVec2> poly;
poly.reserve(STEPS * 2 + 2);
for (int s = 0; s <= STEPS; s++) {
float t = (float)s / (float)STEPS;
float a = a_start[i] + (a_end[i] - a_start[i]) * t;
poly.push_back(ImVec2(center.x + std::cos(a) * r_outer,
center.y + std::sin(a) * r_outer));
}
for (int s = STEPS; s >= 0; s--) {
float t = (float)s / (float)STEPS;
float a = a_start[i] + (a_end[i] - a_start[i]) * t;
poly.push_back(ImVec2(center.x + std::cos(a) * r_inner,
center.y + std::sin(a) * r_inner));
}
dl->AddConvexPolyFilled(poly.data(), (int)poly.size(), col);
}
// Cuerdas: para cada par (i, j) con i <= j, area dentro del arco i proporcional
// a matrix[i,j] / totals[i] del rango angular del nodo i. Idem para j.
// Usamos dos puntos en el arco y bezier hacia el centro.
// Distribuimos sub-arcos por nodo.
std::vector<float> cursor(n, 0.0f);
for (int i = 0; i < n; i++) cursor[i] = a_start[i];
// Pasamos por todas las celdas (incluido diag y j<i): para no duplicar las
// cuerdas, dibujamos solo i<=j combinando matrix[i,j]+matrix[j,i] donde aplica.
// Para simplicidad: dibujamos matrix[i,j] como cuerda i->j independiente (asume simetrica
// o el caller acepta que las cuerdas se solapen).
for (int i = 0; i < n; i++) {
if (totals[i] <= 0.0f) continue;
float arc_i_span = a_end[i] - a_start[i];
for (int j = 0; j < n; j++) {
float v = matrix[i * n + j];
if (v <= 0.0f || i == j) continue;
float frac = v / totals[i];
float a_i_end = cursor[i] + frac * arc_i_span;
// sub-arco en j ocupando proporcion de v / totals[j], partiendo del cursor de j
float arc_j_span = a_end[j] - a_start[j];
float frac_j = (totals[j] > 0.0f) ? (v / totals[j]) : 0.0f;
float a_j_end = cursor[j] + frac_j * arc_j_span;
// 4 puntos en el inner radius
ImVec2 P0(center.x + std::cos(cursor[i]) * r_inner,
center.y + std::sin(cursor[i]) * r_inner);
ImVec2 P1(center.x + std::cos(a_i_end) * r_inner,
center.y + std::sin(a_i_end) * r_inner);
ImVec2 P2(center.x + std::cos(cursor[j]) * r_inner,
center.y + std::sin(cursor[j]) * r_inner);
ImVec2 P3(center.x + std::cos(a_j_end) * r_inner,
center.y + std::sin(a_j_end) * r_inner);
// poligono con bezier cubico desde P1 -> P2 (control hacia el centro) y P3 -> P0
const int STEPS = 24;
std::vector<ImVec2> poly;
poly.reserve(STEPS * 2 + 4);
// arco inner desde P0 -> P1 (en el arco de i)
for (int s = 0; s <= STEPS / 2; s++) {
float t = (float)s / (float)(STEPS / 2);
float a = cursor[i] + (a_i_end - cursor[i]) * t;
poly.push_back(ImVec2(center.x + std::cos(a) * r_inner,
center.y + std::sin(a) * r_inner));
}
// bezier P1 -> P2 con control en center
for (int s = 1; s <= STEPS; s++) {
float t = (float)s / (float)STEPS;
float u = 1.0f - t;
ImVec2 b;
b.x = u*u*u * P1.x + 3*u*u*t * center.x + 3*u*t*t * center.x + t*t*t * P2.x;
b.y = u*u*u * P1.y + 3*u*u*t * center.y + 3*u*t*t * center.y + t*t*t * P2.y;
poly.push_back(b);
}
// arco inner P2 -> P3 (en el arco de j)
for (int s = 1; s <= STEPS / 2; s++) {
float t = (float)s / (float)(STEPS / 2);
float a = cursor[j] + (a_j_end - cursor[j]) * t;
poly.push_back(ImVec2(center.x + std::cos(a) * r_inner,
center.y + std::sin(a) * r_inner));
}
// bezier P3 -> P0
for (int s = 1; s <= STEPS; s++) {
float t = (float)s / (float)STEPS;
float u = 1.0f - t;
ImVec2 b;
b.x = u*u*u * P3.x + 3*u*u*t * center.x + 3*u*t*t * center.x + t*t*t * P0.x;
b.y = u*u*u * P3.y + 3*u*u*t * center.y + 3*u*t*t * center.y + t*t*t * P0.y;
poly.push_back(b);
}
ImU32 col = chord_palette(i);
ImU32 band = (col & 0x00FFFFFF) | (60u << 24);
// ImDrawList exige convexo; un chord no es estrictamente convexo pero
// visualmente queda razonable. Fallback: usar AddPolyline.
dl->AddConvexPolyFilled(poly.data(), (int)poly.size(), band);
cursor[i] = a_i_end;
cursor[j] = a_j_end;
}
}
// Labels alrededor del circulo
if (labels) {
for (int i = 0; i < n; i++) {
float am = (a_start[i] + a_end[i]) * 0.5f;
float lr = r_outer + 8.0f;
float lx = center.x + std::cos(am) * lr;
float ly = center.y + std::sin(am) * lr;
const char* lbl = labels[i] ? labels[i] : "";
ImVec2 ts = ImGui::CalcTextSize(lbl);
// alinear segun el lado
float ax = (std::cos(am) < 0.0f) ? -ts.x : 0.0f;
float ay = -ts.y * 0.5f;
dl->AddText(ImVec2(lx + ax, ly + ay),
IM_COL32(220, 222, 235, 230), lbl);
}
}
ImGui::PopID();
}
+12
View File
@@ -0,0 +1,12 @@
#pragma once
// Chord diagram para matrices N x N de relaciones.
// Renderiza arcos en el borde de un circulo y bandas curvas (bezier) entre arcos.
#include "imgui.h"
void chord(const char* id,
const float* matrix,
int n,
const char* const* labels,
ImVec2 size = ImVec2(400.0f, 400.0f));
+58
View File
@@ -0,0 +1,58 @@
---
name: chord
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "void chord(const char* id, const float* matrix, int n, const char* const* labels, ImVec2 size)"
description: "Chord diagram para matrices NxN de relaciones. Arcos circulares proporcionales a sum(row) + bandas curvas internas (bezier cubico) entre arcos."
tags: [imgui, drawlist, chart, visualization, chord, matrix, relations]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/chord.cpp"
framework: imgui
params:
- name: id
desc: "Identificador unico para PushID"
- name: matrix
desc: "Array NxN row-major (matrix[i*n + j] = flujo de i a j)"
- name: n
desc: "Dimension de la matriz cuadrada"
- name: labels
desc: "Array de N etiquetas, una por entidad. Puede ser nullptr"
- name: size
desc: "Tamano del area cuadrada del chord. Default 400x400"
output: "Renderiza arcos en el borde y cuerdas curvas internas usando AddConvexPolyFilled + AddText"
---
# chord
Chord diagram. Cada nodo ocupa un arco proporcional a la suma de su fila. Las cuerdas representan la magnitud de cada celda matrix[i,j] como bandas curvas (bezier cubico hacia el centro) que conectan el arco de i con el de j.
## Limitaciones
- Las cuerdas se dibujan con `AddConvexPolyFilled` aunque la forma no sea estrictamente convexa — en la practica el renderer ImGui las acepta y queda visualmente razonable.
- Para matrices simetricas se dibuja matrix[i,j] y matrix[j,i] como cuerdas separadas que pueden solaparse. El caller puede pasar la matriz triangulada superior + 0s en la inferior si quiere una cuerda por par.
- Sin interaccion ni tooltip.
## Ejemplo
```cpp
const int N = 4;
float M[N*N] = {
0, 10, 6, 12,
8, 0, 14, 3,
4, 11, 0, 9,
7, 5, 2, 0,
};
const char* labels[N] = {"AAA", "BBB", "CCC", "DDD"};
chord("##flows", M, N, labels);
```
+132
View File
@@ -0,0 +1,132 @@
#include "viz/contour.h"
#include <cmath>
namespace {
// Devuelve la coordenada interpolada [0..1] entre v1 y v2 donde se cruza level.
float interp(float v1, float v2, float level) {
float d = v2 - v1;
if (std::fabs(d) < 1e-9f) return 0.5f;
float t = (level - v1) / d;
if (t < 0.0f) t = 0.0f;
if (t > 1.0f) t = 1.0f;
return t;
}
inline float at(const float* g, int nx, int x, int y) {
return g[y * nx + x];
}
} // namespace
std::vector<ContourLine> contour_compute(const float* grid,
int nx,
int ny,
const float* levels,
int n_levels) {
std::vector<ContourLine> out;
if (!grid || nx < 2 || ny < 2 || !levels || n_levels <= 0) return out;
out.resize(n_levels);
for (int li = 0; li < n_levels; li++) out[li].level = levels[li];
for (int li = 0; li < n_levels; li++) {
float L = levels[li];
auto& line = out[li];
for (int y = 0; y < ny - 1; y++) {
for (int x = 0; x < nx - 1; x++) {
float v00 = at(grid, nx, x, y);
float v10 = at(grid, nx, x + 1, y);
float v11 = at(grid, nx, x + 1, y + 1);
float v01 = at(grid, nx, x, y + 1);
int code = 0;
if (v00 >= L) code |= 1;
if (v10 >= L) code |= 2;
if (v11 >= L) code |= 4;
if (v01 >= L) code |= 8;
if (code == 0 || code == 15) continue;
// Puntos en cada arista (top=between v00 y v10, etc.)
ImVec2 pT(x + interp(v00, v10, L), (float)y);
ImVec2 pR((float)(x + 1), y + interp(v10, v11, L));
ImVec2 pB(x + interp(v01, v11, L), (float)(y + 1));
ImVec2 pL((float)x, y + interp(v00, v01, L));
auto seg = [&](ImVec2 a, ImVec2 b) {
line.pts.push_back(a);
line.pts.push_back(b);
};
switch (code) {
case 1: case 14: seg(pL, pT); break;
case 2: case 13: seg(pT, pR); break;
case 4: case 11: seg(pR, pB); break;
case 8: case 7: seg(pB, pL); break;
case 3: case 12: seg(pL, pR); break;
case 6: case 9: seg(pT, pB); break;
// ambiguos: 5 y 10 — partir en dos segmentos.
case 5: seg(pL, pT); seg(pR, pB); break;
case 10: seg(pT, pR); seg(pB, pL); break;
default: break;
}
}
}
}
return out;
}
void contour(const char* id,
const float* grid,
int nx,
int ny,
const float* levels,
int n_levels,
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));
if (!grid || nx < 2 || ny < 2 || n_levels <= 0) {
ImGui::PopID();
return;
}
auto lines = contour_compute(grid, nx, ny, levels, n_levels);
ImDrawList* dl = ImGui::GetWindowDrawList();
// Borde
dl->AddRect(origin, ImVec2(origin.x + W, origin.y + H),
IM_COL32(80, 80, 90, 200), 0.0f, 0, 1.0f);
float sx = W / (float)(nx - 1);
float sy = H / (float)(ny - 1);
// Color por nivel: gradiente azul -> amarillo
auto level_color = [&](int idx) {
float t = (n_levels > 1) ? (float)idx / (float)(n_levels - 1) : 0.5f;
// (76,140,230) -> (250,200,90)
int r = (int)(76 + (250 - 76) * t);
int g = (int)(140 + (200 - 140) * t);
int b = (int)(230 + (90 - 230) * t);
return IM_COL32(r, g, b, 230);
};
for (int li = 0; li < (int)lines.size(); li++) {
ImU32 col = level_color(li);
const auto& seg = lines[li].pts;
for (size_t k = 0; k + 1 < seg.size(); k += 2) {
ImVec2 a(origin.x + seg[k].x * sx, origin.y + seg[k].y * sy);
ImVec2 b(origin.x + seg[k + 1].x * sx, origin.y + seg[k + 1].y * sy);
dl->AddLine(a, b, col, 1.5f);
}
}
ImGui::PopID();
}
+29
View File
@@ -0,0 +1,29 @@
#pragma once
// Contour plot 2D via marching squares.
// Layout puro (contour_compute) separado del render (contour).
#include "imgui.h"
#include <vector>
struct ContourLine {
std::vector<ImVec2> pts; // segmentos: pts[2k], pts[2k+1] forman un segmento
float level;
};
// Marching squares clasico (16 casos). Para cada nivel, devuelve un ContourLine
// con los segmentos en coords [0..nx-1] x [0..ny-1] del grid (no escaladas).
std::vector<ContourLine> contour_compute(const float* grid,
int nx,
int ny,
const float* levels,
int n_levels);
// Render. size.x <= 0 => ancho disponible. Escala los segmentos a la region dada.
void contour(const char* id,
const float* grid,
int nx,
int ny,
const float* levels,
int n_levels,
ImVec2 size = ImVec2(-1.0f, 300.0f));
+64
View File
@@ -0,0 +1,64 @@
---
name: contour
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "void contour(const char* id, const float* grid, int nx, int ny, const float* levels, int n_levels, ImVec2 size)"
description: "Contour plot 2D usando marching squares clasico (16 casos) con interpolacion lineal entre celdas. Layout puro separado del render."
tags: [imgui, drawlist, chart, visualization, contour, marching-squares, scalar-field]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/contour.cpp"
framework: imgui
params:
- name: id
desc: "Identificador unico para PushID"
- name: grid
desc: "Grid 2D row-major (grid[y*nx + x])"
- name: nx
desc: "Numero de columnas del grid"
- name: ny
desc: "Numero de filas del grid"
- name: levels
desc: "Array de niveles a contornear"
- name: n_levels
desc: "Cantidad de niveles"
- name: size
desc: "Tamano del rect de render. x <= 0 usa el ancho disponible"
output: "Renderiza los contornos como segmentos de linea (AddLine) con color por nivel (gradiente azul->amarillo)"
---
# contour
Marching squares clasico para contornos isovaluados de un grid 2D escalar.
`contour_compute` es pura: para cada nivel devuelve un `ContourLine{pts, level}` donde `pts` es una secuencia de pares (cada par es un segmento). Los puntos estan en coords [0..nx-1] x [0..ny-1] del grid — el render escala a la region.
Casos ambiguos 5 y 10 se resuelven con dos segmentos (sin desambiguar por el centro). Para campos suaves (gaussianas, etc.) este caso es raro.
## Validacion
Para una gaussiana 2D (cumbre en el centro) con varios niveles, los contornos resultantes son anillos cerrados aproximadamente concentricos. Si las isolineas de una gaussiana no se cierran, es un bug del algoritmo.
## Ejemplo
```cpp
constexpr int N = 32;
float grid[N*N];
for (int y = 0; y < N; y++)
for (int x = 0; x < N; x++) {
float dx = x - N/2.0f, dy = y - N/2.0f;
grid[y*N + x] = std::exp(-(dx*dx + dy*dy) / 80.0f);
}
float levels[] = {0.1f, 0.3f, 0.5f, 0.7f, 0.9f};
contour("##gauss", grid, N, N, levels, 5, ImVec2(-1, 300));
```
+233
View File
@@ -0,0 +1,233 @@
#include "viz/sankey.h"
#include <algorithm>
#include <cmath>
#include <queue>
namespace {
// BFS topologico: asigna a cada nodo el max(level(src)+1).
// Nodos sin in-edges arrancan en nivel 0.
std::vector<int> compute_levels(int n_nodes, const std::vector<SankeyLink>& links) {
std::vector<int> indeg(n_nodes, 0);
std::vector<std::vector<int>> out(n_nodes);
for (const auto& l : links) {
if (l.src < 0 || l.src >= n_nodes || l.dst < 0 || l.dst >= n_nodes) continue;
out[l.src].push_back(l.dst);
indeg[l.dst]++;
}
std::vector<int> level(n_nodes, 0);
std::vector<bool> visited(n_nodes, false);
std::queue<int> q;
for (int i = 0; i < n_nodes; i++) {
if (indeg[i] == 0) {
q.push(i);
visited[i] = true;
}
}
while (!q.empty()) {
int u = q.front(); q.pop();
for (int v : out[u]) {
if (level[v] < level[u] + 1) level[v] = level[u] + 1;
indeg[v]--;
if (indeg[v] == 0 && !visited[v]) {
visited[v] = true;
q.push(v);
}
}
}
return level;
}
ImU32 node_color(int idx) {
// Paleta indigo/teal/amber rotativa.
static const ImU32 palette[] = {
IM_COL32(120, 144, 252, 230),
IM_COL32( 92, 200, 200, 230),
IM_COL32(250, 176, 92, 230),
IM_COL32(180, 120, 200, 230),
IM_COL32( 92, 200, 130, 230),
IM_COL32(250, 120, 130, 230),
IM_COL32(180, 200, 92, 230),
IM_COL32(120, 200, 230, 230),
};
constexpr int N = sizeof(palette) / sizeof(palette[0]);
return palette[((idx % N) + N) % N];
}
} // namespace
void sankey(const char* id,
const std::vector<SankeyNode>& nodes,
const std::vector<SankeyLink>& links,
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 : 300.0f;
ImVec2 origin = ImGui::GetCursorScreenPos();
ImGui::Dummy(ImVec2(W, H));
int N = (int)nodes.size();
if (N == 0) { ImGui::PopID(); return; }
auto levels = compute_levels(N, links);
int L = 0;
for (int v : levels) L = std::max(L, v + 1);
if (L < 1) L = 1;
// Magnitud por nodo = max(in_total, out_total). Para sources es out_total,
// para sinks es in_total, en intermedios usamos el max para que los
// rectangulos encajen visualmente.
std::vector<float> in_tot (N, 0.0f);
std::vector<float> out_tot(N, 0.0f);
for (const auto& l : links) {
if (l.src < 0 || l.src >= N || l.dst < 0 || l.dst >= N) continue;
in_tot [l.dst] += l.value;
out_tot[l.src] += l.value;
}
std::vector<float> magnitude(N, 0.0f);
for (int i = 0; i < N; i++) magnitude[i] = std::max(in_tot[i], out_tot[i]);
// Por columna: total magnitud + lista de nodos.
std::vector<std::vector<int>> by_col(L);
for (int i = 0; i < N; i++) by_col[levels[i]].push_back(i);
// Geometria
const float pad_x = 16.0f;
const float pad_y = 16.0f;
const float node_w = 14.0f;
const float gap_y = 6.0f;
float avail_w = std::max(1.0f, W - 2.0f * pad_x);
float col_pitch = (L > 1) ? (avail_w - node_w) / (float)(L - 1) : 0.0f;
float avail_h = std::max(1.0f, H - 2.0f * pad_y);
// Por columna calcula scale: pixel/value tal que (sum mag + gaps) cabe en avail_h.
std::vector<float> col_scale(L, 1.0f);
std::vector<float> col_yoff (L, 0.0f);
for (int c = 0; c < L; c++) {
float sum_m = 0.0f;
for (int i : by_col[c]) sum_m += magnitude[i];
int cnt = (int)by_col[c].size();
float gaps = (cnt > 1) ? (cnt - 1) * gap_y : 0.0f;
float usable = std::max(1.0f, avail_h - gaps);
col_scale[c] = (sum_m > 0.0f) ? (usable / sum_m) : 0.0f;
// Centrar verticalmente
float total_h = sum_m * col_scale[c] + gaps;
col_yoff[c] = pad_y + (avail_h - total_h) * 0.5f;
}
// Posiciones de nodos: para cada nodo, y_top y altura.
struct NodeBox {
float x_left, x_right;
float y_top, y_bot;
};
std::vector<NodeBox> boxes(N);
// y_cursor por columna
std::vector<float> y_cursor(L, 0.0f);
for (int c = 0; c < L; c++) y_cursor[c] = col_yoff[c];
for (int c = 0; c < L; c++) {
for (int i : by_col[c]) {
float h = magnitude[i] * col_scale[c];
if (h < 1.0f) h = 1.0f;
float xl = pad_x + c * col_pitch;
boxes[i].x_left = xl;
boxes[i].x_right = xl + node_w;
boxes[i].y_top = y_cursor[c];
boxes[i].y_bot = y_cursor[c] + h;
y_cursor[c] = boxes[i].y_bot + gap_y;
}
}
// Por nodo: cursores de salida (en src) y entrada (en dst), incrementan al consumir links.
std::vector<float> src_cursor(N, 0.0f);
std::vector<float> dst_cursor(N, 0.0f);
for (int i = 0; i < N; i++) {
src_cursor[i] = boxes[i].y_top;
dst_cursor[i] = boxes[i].y_top;
}
ImDrawList* dl = ImGui::GetWindowDrawList();
// Render links primero (por debajo de los nodos).
// Sort por src para estetica (topdown).
std::vector<int> link_order(links.size());
for (size_t i = 0; i < links.size(); i++) link_order[i] = (int)i;
std::sort(link_order.begin(), link_order.end(),
[&](int a, int b) {
const auto& la = links[a];
const auto& lb = links[b];
if (la.src != lb.src) return la.src < lb.src;
return la.dst < lb.dst;
});
for (int li : link_order) {
const auto& l = links[li];
if (l.src < 0 || l.src >= N || l.dst < 0 || l.dst >= N) continue;
if (l.value <= 0.0f) continue;
float h_src = l.value * col_scale[levels[l.src]];
float h_dst = l.value * col_scale[levels[l.dst]];
float ya0 = src_cursor[l.src];
float ya1 = ya0 + h_src;
float yb0 = dst_cursor[l.dst];
float yb1 = yb0 + h_dst;
src_cursor[l.src] += h_src;
dst_cursor[l.dst] += h_dst;
float xa = boxes[l.src].x_right;
float xb = boxes[l.dst].x_left;
// Dos beziers cubicos formando una banda (top + bottom + cierre).
ImVec2 p0(origin.x + xa, origin.y + (ya0 + ya1) * 0.5f);
ImVec2 p1(origin.x + xb, origin.y + (yb0 + yb1) * 0.5f);
float ctrl_dx = (xb - xa) * 0.5f;
// Aproximar la banda con poligono de 24 segmentos top + 24 bottom.
const int STEPS = 24;
std::vector<ImVec2> poly;
poly.reserve(STEPS * 2 + 2);
for (int s = 0; s <= STEPS; s++) {
float t = (float)s / (float)STEPS;
float u = 1.0f - t;
float bx = u*u*u * (xa) + 3*u*u*t * (xa + ctrl_dx) + 3*u*t*t * (xb - ctrl_dx) + t*t*t * (xb);
float by_top = u*u*u * (ya0) + 3*u*u*t * (ya0) + 3*u*t*t * (yb0) + t*t*t * (yb0);
poly.push_back(ImVec2(origin.x + bx, origin.y + by_top));
(void)p0; (void)p1;
}
for (int s = STEPS; s >= 0; s--) {
float t = (float)s / (float)STEPS;
float u = 1.0f - t;
float bx = u*u*u * (xa) + 3*u*u*t * (xa + ctrl_dx) + 3*u*t*t * (xb - ctrl_dx) + t*t*t * (xb);
float by_bot = u*u*u * (ya1) + 3*u*u*t * (ya1) + 3*u*t*t * (yb1) + t*t*t * (yb1);
poly.push_back(ImVec2(origin.x + bx, origin.y + by_bot));
}
ImU32 src_c = node_color(l.src);
// alpha bajo
ImU32 band = (src_c & 0x00FFFFFF) | (90u << 24);
dl->AddConvexPolyFilled(poly.data(), (int)poly.size(), band);
}
// Render nodos + labels.
for (int i = 0; i < N; i++) {
ImVec2 a(origin.x + boxes[i].x_left, origin.y + boxes[i].y_top);
ImVec2 b(origin.x + boxes[i].x_right, origin.y + boxes[i].y_bot);
dl->AddRectFilled(a, b, node_color(i));
// Label: a la izquierda si es ultima columna, derecha si no.
const char* lbl = nodes[i].label.c_str();
ImVec2 ts = ImGui::CalcTextSize(lbl);
float ly = (a.y + b.y) * 0.5f - ts.y * 0.5f;
if (levels[i] == L - 1) {
// a la izquierda del rect
dl->AddText(ImVec2(a.x - ts.x - 4.0f, ly),
IM_COL32(220, 222, 235, 230), lbl);
} else {
dl->AddText(ImVec2(b.x + 4.0f, ly),
IM_COL32(220, 222, 235, 230), lbl);
}
}
ImGui::PopID();
}
+25
View File
@@ -0,0 +1,25 @@
#pragma once
// Sankey diagram para flujos source -> target con magnitudes.
//
// Asume DAG (sin ciclos). Si hay ciclos, los nodos sin nivel quedan en la
// columna 0 — visualmente raro pero no rompe.
#include "imgui.h"
#include <string>
#include <vector>
struct SankeyNode {
std::string label;
};
struct SankeyLink {
int src;
int dst;
float value;
};
void sankey(const char* id,
const std::vector<SankeyNode>& nodes,
const std::vector<SankeyLink>& links,
ImVec2 size = ImVec2(-1.0f, 400.0f));
+58
View File
@@ -0,0 +1,58 @@
---
name: sankey
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "void sankey(const char* id, const std::vector<SankeyNode>& nodes, const std::vector<SankeyLink>& links, ImVec2 size)"
description: "Sankey diagram para flujos source -> target con magnitudes. BFS topologico para columnas, bandas curvas (bezier cubico) para los links."
tags: [imgui, drawlist, chart, visualization, sankey, flow, dag]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/sankey.cpp"
framework: imgui
params:
- name: id
desc: "Identificador unico para PushID"
- name: nodes
desc: "Vector de SankeyNode (label)"
- name: links
desc: "Vector de SankeyLink {src, dst, value}. src/dst son indices en nodes"
- name: size
desc: "Tamano del diagrama. x <= 0 usa el ancho disponible"
output: "Renderiza nodos como rectangulos verticales por columna y links como bandas con bezier cubico, con alpha bajo y color del nodo origen"
---
# sankey
Sankey diagram. Asigna nodos a columnas via BFS topologico (level = max(level(src))+1) y los apila verticalmente en cada columna proporcionalmente a max(in_total, out_total). Los links se renderizan como bandas curvas con bezier cubico, color del nodo origen + alpha bajo.
## Limitaciones
- **Asume DAG** (sin ciclos). Si hay ciclos, los nodos del ciclo se quedan en su nivel parcial calculado por BFS — el render no rompe pero puede solapar visualmente.
- Sin orden de nodos optimizado para minimizar cruces (heuristica simple por orden de insercion).
- Sin interaccion (hover, click).
## Ejemplo
```cpp
std::vector<SankeyNode> nodes = {
{"clientes_premium"}, {"clientes_basicos"},
{"laptops"}, {"phones"}, {"tablets"},
{"hw"}, {"sw"},
};
std::vector<SankeyLink> links = {
{0, 2, 80}, {0, 3, 30},
{1, 3, 60}, {1, 4, 40},
{2, 5, 80}, {3, 5, 90}, {4, 5, 40},
};
sankey("##flow", nodes, links, ImVec2(-1, 400));
```
+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));
```
+83
View File
@@ -0,0 +1,83 @@
#include "viz/voronoi.h"
#include <cmath>
namespace {
inline int nearest_seed(float px, float py, const ImVec2* seeds, int n) {
int best = 0;
float bd2 = 1e30f;
for (int i = 0; i < n; i++) {
float dx = px - seeds[i].x;
float dy = py - seeds[i].y;
float d2 = dx * dx + dy * dy;
if (d2 < bd2) { bd2 = d2; best = i; }
}
return best;
}
} // namespace
std::vector<VoronoiCell> voronoi_layout(const ImVec2* seeds, int n, ImVec2 /*region*/) {
std::vector<VoronoiCell> out;
if (!seeds || n <= 0) return out;
out.resize(n);
for (int i = 0; i < n; i++) {
out[i].seed = seeds[i];
out[i].color = IM_COL32(120, 144, 252, 230);
out[i].polygon.clear();
}
return out;
}
void voronoi(const char* id,
const ImVec2* seeds,
int n,
const ImU32* colors,
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));
if (!seeds || n <= 0) { ImGui::PopID(); return; }
ImDrawList* dl = ImGui::GetWindowDrawList();
// Raster: tile de 4x4 pixels. Region <= ~600x400 con N<=200 -> ~150*100 = 15000 calls,
// dentro de presupuesto frame.
const float tile = 4.0f;
int cols = (int)std::ceil(W / tile);
int rows = (int)std::ceil(H / tile);
for (int ry = 0; ry < rows; ry++) {
float py = (ry + 0.5f) * tile;
if (py > H) py = H;
for (int rx = 0; rx < cols; rx++) {
float px = (rx + 0.5f) * tile;
if (px > W) px = W;
int idx = nearest_seed(px, py, seeds, n);
ImU32 col = colors ? colors[idx] : IM_COL32(120, 144, 252, 230);
ImVec2 a(origin.x + rx * tile, origin.y + ry * tile);
ImVec2 b(origin.x + std::min((rx + 1) * tile, W),
origin.y + std::min((ry + 1) * tile, H));
dl->AddRectFilled(a, b, col);
}
}
// Borde y seeds
dl->AddRect(origin, ImVec2(origin.x + W, origin.y + H),
IM_COL32(80, 80, 90, 220), 0.0f, 0, 1.0f);
for (int i = 0; i < n; i++) {
ImVec2 c(origin.x + seeds[i].x, origin.y + seeds[i].y);
dl->AddCircleFilled(c, 2.5f, IM_COL32(20, 22, 28, 255));
dl->AddCircleFilled(c, 1.5f, IM_COL32(245, 246, 250, 255));
}
ImGui::PopID();
}
+26
View File
@@ -0,0 +1,26 @@
#pragma once
// Diagrama de Voronoi via raster brute-force (MVP).
// Para cada pixel, encontrar el seed mas cercano (Euclidea). Suficiente para
// N <= 200 seeds y region <= 500x500.
#include "imgui.h"
#include <vector>
struct VoronoiCell {
std::vector<ImVec2> polygon; // En MVP raster queda vacio. Lo dejamos por API future-proof.
ImVec2 seed;
ImU32 color;
};
// Layout puro. En MVP raster no devuelve poligonos analiticos: el polygon
// queda vacio y solo se rellenan seed/color. El render usa raster directo.
std::vector<VoronoiCell> voronoi_layout(const ImVec2* seeds, int n, ImVec2 region);
// Render brute-force: muestrea region en celdas de px_size pixeles, para cada
// celda escribe un AddRectFilled con el color del seed mas cercano.
void voronoi(const char* id,
const ImVec2* seeds,
int n,
const ImU32* colors,
ImVec2 size = ImVec2(-1.0f, 300.0f));
+56
View File
@@ -0,0 +1,56 @@
---
name: voronoi
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "void voronoi(const char* id, const ImVec2* seeds, int n, const ImU32* colors, ImVec2 size)"
description: "Diagrama de Voronoi via raster brute-force (MVP). Para cada tile 4x4 px encontrar seed mas cercano y rellenar con su color."
tags: [imgui, drawlist, chart, visualization, voronoi, raster]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/voronoi.cpp"
framework: imgui
params:
- name: id
desc: "Identificador unico para PushID"
- name: seeds
desc: "Array de N posiciones (en coords locales del rect, [0..W]x[0..H])"
- name: n
desc: "Numero de seeds"
- name: colors
desc: "Array de N colores ImU32 (uno por seed). Si nullptr, usa color default"
- name: size
desc: "Tamano del area Voronoi. x <= 0 usa el ancho disponible"
output: "Renderiza la teselacion de Voronoi como mosaico de tiles 4x4 px coloreados + seeds visibles como circulos blancos sobre negros"
---
# voronoi
Voronoi diagram MVP via raster brute-force. Para cada tile de 4x4 pixeles del rect de render, calcula el seed mas cercano por distancia Euclidea y rellena el tile con el color de ese seed.
## Limitaciones MVP
- **No genera poligonos analiticos** — `voronoi_layout` deja `polygon` vacio. Para extraer poligonos seria necesario half-plane intersections (Fortune's algorithm) — ver issue futuro.
- **Tile 4x4 px**: las fronteras son escalonadas. Si se requiere precision sub-pixel, reducir el tile (cuesta O(1/tile^2) en tiempo).
- **Performance**: O(W * H * N / tile^2). Para N <= 200 y region 500x500 es < 1ms.
## Ejemplo
```cpp
ImVec2 seeds[30];
ImU32 colors[30];
for (int i = 0; i < 30; i++) {
seeds[i] = ImVec2(rand_float() * W, rand_float() * H);
colors[i] = IM_COL32(rand() & 255, rand() & 255, rand() & 255, 230);
}
voronoi("##v", seeds, 30, colors, ImVec2(-1, 300));
```