fix(fn-run): propagar stdout/stderr de bash functions library-style #1
@@ -0,0 +1,233 @@
|
||||
#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();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
// Sankey diagram para flujos source -> target con magnitudes.
|
||||
//
|
||||
// Asume DAG (sin ciclos). Si hay ciclos, los nodos sin nivel quedan en la
|
||||
// columna 0 — visualmente raro pero no rompe.
|
||||
|
||||
#include "imgui.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct SankeyNode {
|
||||
std::string label;
|
||||
};
|
||||
|
||||
struct SankeyLink {
|
||||
int src;
|
||||
int dst;
|
||||
float value;
|
||||
};
|
||||
|
||||
void sankey(const char* id,
|
||||
const std::vector<SankeyNode>& nodes,
|
||||
const std::vector<SankeyLink>& links,
|
||||
ImVec2 size = ImVec2(-1.0f, 400.0f));
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: sankey
|
||||
kind: component
|
||||
lang: cpp
|
||||
domain: viz
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "void sankey(const char* id, const std::vector<SankeyNode>& nodes, const std::vector<SankeyLink>& links, ImVec2 size)"
|
||||
description: "Sankey diagram para flujos source -> target con magnitudes. BFS topologico para columnas, bandas curvas (bezier cubico) para los links."
|
||||
tags: [imgui, drawlist, chart, visualization, sankey, flow, dag]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: [imgui]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "cpp/functions/viz/sankey.cpp"
|
||||
framework: imgui
|
||||
params:
|
||||
- name: id
|
||||
desc: "Identificador unico para PushID"
|
||||
- name: nodes
|
||||
desc: "Vector de SankeyNode (label)"
|
||||
- name: links
|
||||
desc: "Vector de SankeyLink {src, dst, value}. src/dst son indices en nodes"
|
||||
- name: size
|
||||
desc: "Tamano del diagrama. x <= 0 usa el ancho disponible"
|
||||
output: "Renderiza nodos como rectangulos verticales por columna y links como bandas con bezier cubico, con alpha bajo y color del nodo origen"
|
||||
---
|
||||
|
||||
# sankey
|
||||
|
||||
Sankey diagram. Asigna nodos a columnas via BFS topologico (level = max(level(src))+1) y los apila verticalmente en cada columna proporcionalmente a max(in_total, out_total). Los links se renderizan como bandas curvas con bezier cubico, color del nodo origen + alpha bajo.
|
||||
|
||||
## Limitaciones
|
||||
|
||||
- **Asume DAG** (sin ciclos). Si hay ciclos, los nodos del ciclo se quedan en su nivel parcial calculado por BFS — el render no rompe pero puede solapar visualmente.
|
||||
- Sin orden de nodos optimizado para minimizar cruces (heuristica simple por orden de insercion).
|
||||
- Sin interaccion (hover, click).
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```cpp
|
||||
std::vector<SankeyNode> nodes = {
|
||||
{"clientes_premium"}, {"clientes_basicos"},
|
||||
{"laptops"}, {"phones"}, {"tablets"},
|
||||
{"hw"}, {"sw"},
|
||||
};
|
||||
std::vector<SankeyLink> links = {
|
||||
{0, 2, 80}, {0, 3, 30},
|
||||
{1, 3, 60}, {1, 4, 40},
|
||||
{2, 5, 80}, {3, 5, 90}, {4, 5, 40},
|
||||
};
|
||||
sankey("##flow", nodes, links, ImVec2(-1, 400));
|
||||
```
|
||||
Reference in New Issue
Block a user