64330944e1
compute_levels asigna columnas via BFS, los nodos se apilan verticalmente proporcional a max(in_total, out_total). Los links se renderizan como bandas con bezier cubico, color del nodo origen + alpha bajo. Asume DAG (sin ciclos). Si hay ciclos, los nodos del ciclo quedan en su nivel parcial — no rompe pero puede solapar visualmente.
234 lines
8.4 KiB
C++
234 lines
8.4 KiB
C++
#include "viz/sankey.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <queue>
|
|
|
|
namespace {
|
|
|
|
// BFS topologico: asigna a cada nodo el max(level(src)+1).
|
|
// Nodos sin in-edges arrancan en nivel 0.
|
|
std::vector<int> compute_levels(int n_nodes, const std::vector<SankeyLink>& links) {
|
|
std::vector<int> indeg(n_nodes, 0);
|
|
std::vector<std::vector<int>> out(n_nodes);
|
|
for (const auto& l : links) {
|
|
if (l.src < 0 || l.src >= n_nodes || l.dst < 0 || l.dst >= n_nodes) continue;
|
|
out[l.src].push_back(l.dst);
|
|
indeg[l.dst]++;
|
|
}
|
|
std::vector<int> level(n_nodes, 0);
|
|
std::vector<bool> visited(n_nodes, false);
|
|
std::queue<int> q;
|
|
for (int i = 0; i < n_nodes; i++) {
|
|
if (indeg[i] == 0) {
|
|
q.push(i);
|
|
visited[i] = true;
|
|
}
|
|
}
|
|
while (!q.empty()) {
|
|
int u = q.front(); q.pop();
|
|
for (int v : out[u]) {
|
|
if (level[v] < level[u] + 1) level[v] = level[u] + 1;
|
|
indeg[v]--;
|
|
if (indeg[v] == 0 && !visited[v]) {
|
|
visited[v] = true;
|
|
q.push(v);
|
|
}
|
|
}
|
|
}
|
|
return level;
|
|
}
|
|
|
|
ImU32 node_color(int idx) {
|
|
// Paleta indigo/teal/amber rotativa.
|
|
static const ImU32 palette[] = {
|
|
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(palette) / sizeof(palette[0]);
|
|
return palette[((idx % N) + N) % N];
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void sankey(const char* id,
|
|
const std::vector<SankeyNode>& nodes,
|
|
const std::vector<SankeyLink>& links,
|
|
ImVec2 size) {
|
|
ImGui::PushID(id);
|
|
|
|
ImVec2 avail = ImGui::GetContentRegionAvail();
|
|
float W = (size.x > 0.0f) ? size.x : avail.x;
|
|
float H = (size.y > 0.0f) ? size.y : 300.0f;
|
|
|
|
ImVec2 origin = ImGui::GetCursorScreenPos();
|
|
ImGui::Dummy(ImVec2(W, H));
|
|
|
|
int N = (int)nodes.size();
|
|
if (N == 0) { ImGui::PopID(); return; }
|
|
|
|
auto levels = compute_levels(N, links);
|
|
int L = 0;
|
|
for (int v : levels) L = std::max(L, v + 1);
|
|
if (L < 1) L = 1;
|
|
|
|
// Magnitud por nodo = max(in_total, out_total). Para sources es out_total,
|
|
// para sinks es in_total, en intermedios usamos el max para que los
|
|
// rectangulos encajen visualmente.
|
|
std::vector<float> in_tot (N, 0.0f);
|
|
std::vector<float> out_tot(N, 0.0f);
|
|
for (const auto& l : links) {
|
|
if (l.src < 0 || l.src >= N || l.dst < 0 || l.dst >= N) continue;
|
|
in_tot [l.dst] += l.value;
|
|
out_tot[l.src] += l.value;
|
|
}
|
|
std::vector<float> magnitude(N, 0.0f);
|
|
for (int i = 0; i < N; i++) magnitude[i] = std::max(in_tot[i], out_tot[i]);
|
|
|
|
// Por columna: total magnitud + lista de nodos.
|
|
std::vector<std::vector<int>> by_col(L);
|
|
for (int i = 0; i < N; i++) by_col[levels[i]].push_back(i);
|
|
|
|
// Geometria
|
|
const float pad_x = 16.0f;
|
|
const float pad_y = 16.0f;
|
|
const float node_w = 14.0f;
|
|
const float gap_y = 6.0f;
|
|
float avail_w = std::max(1.0f, W - 2.0f * pad_x);
|
|
float col_pitch = (L > 1) ? (avail_w - node_w) / (float)(L - 1) : 0.0f;
|
|
float avail_h = std::max(1.0f, H - 2.0f * pad_y);
|
|
|
|
// Por columna calcula scale: pixel/value tal que (sum mag + gaps) cabe en avail_h.
|
|
std::vector<float> col_scale(L, 1.0f);
|
|
std::vector<float> col_yoff (L, 0.0f);
|
|
for (int c = 0; c < L; c++) {
|
|
float sum_m = 0.0f;
|
|
for (int i : by_col[c]) sum_m += magnitude[i];
|
|
int cnt = (int)by_col[c].size();
|
|
float gaps = (cnt > 1) ? (cnt - 1) * gap_y : 0.0f;
|
|
float usable = std::max(1.0f, avail_h - gaps);
|
|
col_scale[c] = (sum_m > 0.0f) ? (usable / sum_m) : 0.0f;
|
|
// Centrar verticalmente
|
|
float total_h = sum_m * col_scale[c] + gaps;
|
|
col_yoff[c] = pad_y + (avail_h - total_h) * 0.5f;
|
|
}
|
|
|
|
// Posiciones de nodos: para cada nodo, y_top y altura.
|
|
struct NodeBox {
|
|
float x_left, x_right;
|
|
float y_top, y_bot;
|
|
};
|
|
std::vector<NodeBox> boxes(N);
|
|
// y_cursor por columna
|
|
std::vector<float> y_cursor(L, 0.0f);
|
|
for (int c = 0; c < L; c++) y_cursor[c] = col_yoff[c];
|
|
for (int c = 0; c < L; c++) {
|
|
for (int i : by_col[c]) {
|
|
float h = magnitude[i] * col_scale[c];
|
|
if (h < 1.0f) h = 1.0f;
|
|
float xl = pad_x + c * col_pitch;
|
|
boxes[i].x_left = xl;
|
|
boxes[i].x_right = xl + node_w;
|
|
boxes[i].y_top = y_cursor[c];
|
|
boxes[i].y_bot = y_cursor[c] + h;
|
|
y_cursor[c] = boxes[i].y_bot + gap_y;
|
|
}
|
|
}
|
|
|
|
// Por nodo: cursores de salida (en src) y entrada (en dst), incrementan al consumir links.
|
|
std::vector<float> src_cursor(N, 0.0f);
|
|
std::vector<float> dst_cursor(N, 0.0f);
|
|
for (int i = 0; i < N; i++) {
|
|
src_cursor[i] = boxes[i].y_top;
|
|
dst_cursor[i] = boxes[i].y_top;
|
|
}
|
|
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
|
|
// Render links primero (por debajo de los nodos).
|
|
// Sort por src para estetica (topdown).
|
|
std::vector<int> link_order(links.size());
|
|
for (size_t i = 0; i < links.size(); i++) link_order[i] = (int)i;
|
|
std::sort(link_order.begin(), link_order.end(),
|
|
[&](int a, int b) {
|
|
const auto& la = links[a];
|
|
const auto& lb = links[b];
|
|
if (la.src != lb.src) return la.src < lb.src;
|
|
return la.dst < lb.dst;
|
|
});
|
|
|
|
for (int li : link_order) {
|
|
const auto& l = links[li];
|
|
if (l.src < 0 || l.src >= N || l.dst < 0 || l.dst >= N) continue;
|
|
if (l.value <= 0.0f) continue;
|
|
|
|
float h_src = l.value * col_scale[levels[l.src]];
|
|
float h_dst = l.value * col_scale[levels[l.dst]];
|
|
float ya0 = src_cursor[l.src];
|
|
float ya1 = ya0 + h_src;
|
|
float yb0 = dst_cursor[l.dst];
|
|
float yb1 = yb0 + h_dst;
|
|
src_cursor[l.src] += h_src;
|
|
dst_cursor[l.dst] += h_dst;
|
|
|
|
float xa = boxes[l.src].x_right;
|
|
float xb = boxes[l.dst].x_left;
|
|
|
|
// Dos beziers cubicos formando una banda (top + bottom + cierre).
|
|
ImVec2 p0(origin.x + xa, origin.y + (ya0 + ya1) * 0.5f);
|
|
ImVec2 p1(origin.x + xb, origin.y + (yb0 + yb1) * 0.5f);
|
|
float ctrl_dx = (xb - xa) * 0.5f;
|
|
|
|
// Aproximar la banda con poligono de 24 segmentos top + 24 bottom.
|
|
const int STEPS = 24;
|
|
std::vector<ImVec2> poly;
|
|
poly.reserve(STEPS * 2 + 2);
|
|
for (int s = 0; s <= STEPS; s++) {
|
|
float t = (float)s / (float)STEPS;
|
|
float u = 1.0f - t;
|
|
float bx = u*u*u * (xa) + 3*u*u*t * (xa + ctrl_dx) + 3*u*t*t * (xb - ctrl_dx) + t*t*t * (xb);
|
|
float by_top = u*u*u * (ya0) + 3*u*u*t * (ya0) + 3*u*t*t * (yb0) + t*t*t * (yb0);
|
|
poly.push_back(ImVec2(origin.x + bx, origin.y + by_top));
|
|
(void)p0; (void)p1;
|
|
}
|
|
for (int s = STEPS; s >= 0; s--) {
|
|
float t = (float)s / (float)STEPS;
|
|
float u = 1.0f - t;
|
|
float bx = u*u*u * (xa) + 3*u*u*t * (xa + ctrl_dx) + 3*u*t*t * (xb - ctrl_dx) + t*t*t * (xb);
|
|
float by_bot = u*u*u * (ya1) + 3*u*u*t * (ya1) + 3*u*t*t * (yb1) + t*t*t * (yb1);
|
|
poly.push_back(ImVec2(origin.x + bx, origin.y + by_bot));
|
|
}
|
|
ImU32 src_c = node_color(l.src);
|
|
// alpha bajo
|
|
ImU32 band = (src_c & 0x00FFFFFF) | (90u << 24);
|
|
dl->AddConvexPolyFilled(poly.data(), (int)poly.size(), band);
|
|
}
|
|
|
|
// Render nodos + labels.
|
|
for (int i = 0; i < N; i++) {
|
|
ImVec2 a(origin.x + boxes[i].x_left, origin.y + boxes[i].y_top);
|
|
ImVec2 b(origin.x + boxes[i].x_right, origin.y + boxes[i].y_bot);
|
|
dl->AddRectFilled(a, b, node_color(i));
|
|
// Label: a la izquierda si es ultima columna, derecha si no.
|
|
const char* lbl = nodes[i].label.c_str();
|
|
ImVec2 ts = ImGui::CalcTextSize(lbl);
|
|
float ly = (a.y + b.y) * 0.5f - ts.y * 0.5f;
|
|
if (levels[i] == L - 1) {
|
|
// a la izquierda del rect
|
|
dl->AddText(ImVec2(a.x - ts.x - 4.0f, ly),
|
|
IM_COL32(220, 222, 235, 230), lbl);
|
|
} else {
|
|
dl->AddText(ImVec2(b.x + 4.0f, ly),
|
|
IM_COL32(220, 222, 235, 230), lbl);
|
|
}
|
|
}
|
|
|
|
ImGui::PopID();
|
|
}
|