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:
@@ -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();
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
@@ -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);
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user