feat(viz): chord diagram — arcos circulares + cuerdas bezier

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.
This commit is contained in:
2026-04-25 21:52:43 +02:00
parent 64330944e1
commit 071aa71a04
3 changed files with 270 additions and 0 deletions
+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);
```