75d4334e8c
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.
201 lines
7.9 KiB
C++
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();
|
|
}
|