Files
egutierrez 75d4334e8c 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.
2026-04-25 21:52:43 +02:00

201 lines
7.9 KiB
C++

#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();
}