fix(fn-run): propagar stdout/stderr de bash functions library-style #1

Open
dataforge wants to merge 537 commits from auto/0077-fn-run-bash-mudo into master
3 changed files with 316 additions and 0 deletions
Showing only changes of commit 64330944e1 - Show all commits
+233
View File
@@ -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();
}
+25
View File
@@ -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));
+58
View File
@@ -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));
```