From 75d4334e8c3c29bd2e44a32854c4866f49bf9f52 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 25 Apr 2026 21:52:43 +0200 Subject: [PATCH] =?UTF-8?q?feat(viz):=20chord=20diagram=20=E2=80=94=20arco?= =?UTF-8?q?s=20circulares=20+=20cuerdas=20bezier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Para una matriz NxN: cada nodo ocupa un arco proporcional a sum(row). Las cuerdas matrix[i,j] son bandas bezier cubico hacia el centro conectando los arcos de i y j. Limitacion: las cuerdas se dibujan con AddConvexPolyFilled aunque la forma no sea estrictamente convexa — visualmente queda razonable. --- cpp/functions/viz/chord.cpp | 200 ++++++++++++++++++++++++++++++++++++ cpp/functions/viz/chord.h | 12 +++ cpp/functions/viz/chord.md | 58 +++++++++++ 3 files changed, 270 insertions(+) create mode 100644 cpp/functions/viz/chord.cpp create mode 100644 cpp/functions/viz/chord.h create mode 100644 cpp/functions/viz/chord.md diff --git a/cpp/functions/viz/chord.cpp b/cpp/functions/viz/chord.cpp new file mode 100644 index 00000000..df4ea1b7 --- /dev/null +++ b/cpp/functions/viz/chord.cpp @@ -0,0 +1,200 @@ +#include "viz/chord.h" + +#include +#include + +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 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 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 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 cursor(n, 0.0f); + for (int i = 0; i < n; i++) cursor[i] = a_start[i]; + + // Pasamos por todas las celdas (incluido diag y jj 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 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(); +} diff --git a/cpp/functions/viz/chord.h b/cpp/functions/viz/chord.h new file mode 100644 index 00000000..26d58f45 --- /dev/null +++ b/cpp/functions/viz/chord.h @@ -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)); diff --git a/cpp/functions/viz/chord.md b/cpp/functions/viz/chord.md new file mode 100644 index 00000000..91e74b9e --- /dev/null +++ b/cpp/functions/viz/chord.md @@ -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); +```