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