Files
egutierrez a03675113a chore: auto-commit (286 archivos)
- .claude/agents/fn-orquestador/SKILL.md
- .claude/commands/fn_claude.md
- .claude/rules/INDEX.md
- .claude/rules/cpp_apps.md
- .claude/rules/ids_naming.md
- CHANGELOG.md
- apps/dag_engine/README.md
- apps/dag_engine/api.go
- apps/dag_engine/dags_migrated/example.yaml
- apps/dag_engine/dags_migrated/example_lineage_tracking.yaml
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 16:33:22 +02:00

925 lines
38 KiB
C++

#include "viz/viz_render.h"
#include "core/auto_detect_type.h" // parse_number
#include "implot.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
#include <unordered_map>
#include <vector>
namespace viz {
using data_table::StageOutput;
using data_table::ColumnType;
using data_table::ViewMode;
using data_table::ViewConfig;
using data_table::parse_number;
// Hit-test helpers (pure math; extracted from data_table_logic).
namespace {
// Returns index of nearest point in xs[]/ys[] to (tx,ty). -1 if n==0.
static int nearest_index_2d(double tx, double ty,
const double* xs, const double* ys, int n) {
int best = -1;
double best_d = 1e300;
for (int i = 0; i < n; ++i) {
double dx = xs[i] - tx, dy = ys[i] - ty;
double d = dx*dx + dy*dy;
if (d < best_d) { best_d = d; best = i; }
}
return best;
}
// Returns angle in [0, 360) CCW from top (12 o'clock) for point (mx,my)
// relative to center (cx,cy).
static double pie_angle(double cx, double cy, double mx, double my) {
constexpr double kPI = 3.14159265358979323846;
double dx = mx - cx, dy = my - cy;
double angle_deg = std::atan2(dy, dx) * 180.0 / kPI;
double offset = angle_deg - 90.0;
while (offset < 0.0) offset += 360.0;
while (offset >= 360.0) offset -= 360.0;
return offset;
}
// Returns slice index for angle (deg, [0,360)) in a pie of n slices with
// given `values`. Returns n-1 for last slice. -1 if n==0.
static int pie_slice_at_angle(double angle, const double* values, int n) {
if (n <= 0) return -1;
double total = 0.0;
for (int i = 0; i < n; ++i) total += values[i];
if (total == 0.0) return 0;
double acc = 0.0;
for (int i = 0; i < n; ++i) {
double sweep = (values[i] / total) * 360.0;
if (angle >= acc && angle < acc + sweep) return i;
acc += sweep;
}
return n - 1;
}
// Maps plot coordinates (px,py) in a heatmap (rows x cols, y=0..rows x=0..cols)
// to (row_out, col_out). Sets -1 if out of range.
static void heatmap_cell_at(double px, double py, int rows, int cols,
int& row_out, int& col_out) {
col_out = (int)std::floor(px);
row_out = (int)std::floor(py);
if (row_out < 0 || row_out >= rows || col_out < 0 || col_out >= cols) {
row_out = col_out = -1;
}
}
} // anon (hit-test helpers)
static int find_header(const StageOutput& out, const std::string& name) {
if (name.empty()) return -1;
for (size_t c = 0; c < out.headers.size(); ++c)
if (out.headers[c] == name) return (int)c;
return -1;
}
static int resolve_x(const StageOutput& out, const ViewConfig& cfg, int fallback) {
int c = find_header(out, cfg.x_col);
return (c >= 0) ? c : fallback;
}
static int resolve_cat(const StageOutput& out, const ViewConfig& cfg, int fallback) {
int c = find_header(out, cfg.cat_col);
return (c >= 0) ? c : fallback;
}
static int resolve_size(const StageOutput& out, const ViewConfig& cfg, int fallback) {
int c = find_header(out, cfg.size_col);
return (c >= 0) ? c : fallback;
}
int first_numeric_col(const StageOutput& out) {
for (size_t c = 0; c < out.types.size(); ++c) {
if (out.types[c] == ColumnType::Int || out.types[c] == ColumnType::Float) return (int)c;
}
return -1;
}
int first_category_col(const StageOutput& out) {
for (size_t c = 0; c < out.types.size(); ++c) {
ColumnType t = out.types[c];
if (t == ColumnType::String || t == ColumnType::Date || t == ColumnType::Bool ||
t == ColumnType::Json) return (int)c;
}
return -1;
}
std::vector<double> extract_numeric(const StageOutput& out, int col) {
std::vector<double> v;
if (col < 0 || col >= out.cols) return v;
v.reserve(out.rows);
for (int r = 0; r < out.rows; ++r) {
const char* s = out.cells[(size_t)r * out.cols + col];
double d = 0;
if (s && *s && parse_number(s, d)) v.push_back(d);
else v.push_back(std::nan(""));
}
return v;
}
std::vector<std::string> extract_category(const StageOutput& out, int col) {
std::vector<std::string> v;
if (col < 0 || col >= out.cols) return v;
v.reserve(out.rows);
for (int r = 0; r < out.rows; ++r) {
const char* s = out.cells[(size_t)r * out.cols + col];
v.emplace_back(s ? s : "");
}
return v;
}
namespace {
struct NumCol { int idx; std::string name; std::vector<double> vals; };
std::vector<NumCol> collect_numeric(const StageOutput& out, int max_n = 16) {
std::vector<NumCol> r;
for (size_t c = 0; c < out.types.size() && (int)r.size() < max_n; ++c) {
if (out.types[c] == ColumnType::Int || out.types[c] == ColumnType::Float) {
NumCol nc;
nc.idx = (int)c;
nc.name = out.headers[c];
nc.vals = extract_numeric(out, (int)c);
r.push_back(std::move(nc));
}
}
return r;
}
std::vector<NumCol> collect_numeric_filtered(const StageOutput& out,
const ViewConfig& cfg,
int max_n = 16) {
if (cfg.y_cols.empty()) return collect_numeric(out, max_n);
std::vector<NumCol> r;
for (const auto& name : cfg.y_cols) {
if ((int)r.size() >= max_n) break;
int c = find_header(out, name);
if (c < 0) continue;
if (out.types[c] != ColumnType::Int && out.types[c] != ColumnType::Float) continue;
NumCol nc;
nc.idx = c;
nc.name = out.headers[c];
nc.vals = extract_numeric(out, c);
r.push_back(std::move(nc));
}
if (r.empty()) return collect_numeric(out, max_n);
return r;
}
ImPlotSpec spec_with_color(unsigned int rgba_color) {
if (rgba_color == 0) return ImPlotSpec();
ImU32 c = (ImU32)rgba_color;
return ImPlotSpec(ImPlotProp_LineColor, c, ImPlotProp_FillColor, c);
}
// Axis flags: locked = no pan/zoom; unlocked = 0 (sin AutoFit, para preservar
// pan/zoom del user). Re-fit explicito via SetNextAxesToFit cuando fit_request.
ImPlotAxisFlags axflag(const ViewConfig& cfg, ImPlotAxisFlags base = 0) {
if (cfg.locked) return base | ImPlotAxisFlags_Lock;
return base;
}
// Llamar antes de BeginPlot. Si cfg.fit_request -> fuerza re-fit y limpia el flag.
void maybe_fit(const ViewConfig& cfg) {
if (cfg.fit_request) {
ImPlot::SetNextAxesToFit();
cfg.fit_request = false;
}
}
void info_text(const char* msg) {
ImVec2 avail = ImGui::GetContentRegionAvail();
ImVec2 sz = ImGui::CalcTextSize(msg);
ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + (avail.x - sz.x) * 0.5f,
ImGui::GetCursorPosY() + (avail.y - sz.y) * 0.5f));
ImGui::TextDisabled("%s", msg);
}
// Drop NaN and pair with optional labels.
std::vector<double> finite(const std::vector<double>& v) {
std::vector<double> r; r.reserve(v.size());
for (double d : v) if (!std::isnan(d)) r.push_back(d);
return r;
}
bool render_bar_like(const StageOutput& out, ViewMode mode,
const ViewConfig& cfg, ImVec2 size,
int* clicked_row_out = nullptr) {
int cat_col = resolve_cat(out, cfg, first_category_col(out));
auto nums = collect_numeric_filtered(out, cfg, 8);
if (cat_col < 0 || nums.empty()) {
info_text("Need 1 category + 1+ numeric columns");
return false;
}
auto cats = extract_category(out, cat_col);
int n = (int)cats.size();
if (n == 0) { info_text("Empty data"); return false; }
// Ticks
std::vector<double> ticks(n);
std::vector<const char*> labels(n);
for (int i = 0; i < n; ++i) { ticks[i] = i; labels[i] = cats[i].c_str(); }
bool horiz = (mode == ViewMode::Bar);
ImPlotFlags pflags = cfg.show_legend ? 0 : ImPlotFlags_NoLegend;
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##bar", size, pflags)) return false;
ImPlotAxisFlags ax_cat = axflag(cfg);
ImPlotAxisFlags ax_num = axflag(cfg);
if (horiz) {
ImPlot::SetupAxes(out.headers[nums[0].idx].c_str(), out.headers[cat_col].c_str(),
ax_num, ax_cat);
ImPlot::SetupAxisTicks(ImAxis_Y1, ticks.data(), n, labels.data(), false);
} else {
ImPlot::SetupAxes(out.headers[cat_col].c_str(), out.headers[nums[0].idx].c_str(),
ax_cat, ax_num);
ImPlot::SetupAxisTicks(ImAxis_X1, ticks.data(), n, labels.data(), false);
}
if (mode == ViewMode::StackedBar || mode == ViewMode::GroupedBar) {
// Build flat matrix items x groups
int items = (int)nums.size();
std::vector<double> mat((size_t)items * n, 0.0);
std::vector<const char*> series_labels(items);
for (int it = 0; it < items; ++it) {
series_labels[it] = nums[it].name.c_str();
for (int g = 0; g < n; ++g) {
double d = nums[it].vals[g];
mat[(size_t)it * n + g] = std::isnan(d) ? 0.0 : d;
}
}
int flags = (mode == ViewMode::StackedBar) ? ImPlotBarGroupsFlags_Stacked : 0;
if (horiz) flags |= ImPlotBarGroupsFlags_Horizontal;
ImPlot::PlotBarGroups(series_labels.data(), mat.data(), items, n, 0.67, 0,
ImPlotSpec(ImPlotProp_Flags, flags));
} else {
// Single series (first numeric col).
std::vector<double> ys(n);
for (int i = 0; i < n; ++i) {
double d = nums[0].vals[i];
ys[i] = std::isnan(d) ? 0.0 : d;
}
ImPlotSpec spc = spec_with_color(cfg.primary_color);
if (horiz) {
if (cfg.primary_color != 0) {
ImU32 col = (ImU32)cfg.primary_color;
ImPlot::PlotBars(nums[0].name.c_str(), ys.data(), ticks.data(), n, 0.67,
ImPlotSpec(ImPlotProp_Flags, ImPlotBarsFlags_Horizontal,
ImPlotProp_FillColor, col,
ImPlotProp_LineColor, col));
} else {
ImPlot::PlotBars(nums[0].name.c_str(), ys.data(), ticks.data(), n, 0.67,
ImPlotSpec(ImPlotProp_Flags, ImPlotBarsFlags_Horizontal));
}
} else {
ImPlot::PlotBars(nums[0].name.c_str(), ticks.data(), ys.data(), n, 0.67, spc);
}
}
// Hit-test fase 10: idx = round(plot.{x|y}) en single-series mode.
if (clicked_row_out &&
mode != ViewMode::GroupedBar && mode != ViewMode::StackedBar &&
ImPlot::IsPlotHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
ImPlotPoint p = ImPlot::GetPlotMousePos();
double target = horiz ? p.y : p.x;
int idx = (int)(target + 0.5);
if (idx >= 0 && idx < n) *clicked_row_out = idx;
}
ImPlot::EndPlot();
return true;
}
bool render_line_like(const StageOutput& out, ViewMode mode,
const ViewConfig& cfg, ImVec2 size) {
auto nums = collect_numeric_filtered(out, cfg, 8);
if (nums.empty()) { info_text("Need at least 1 numeric column"); return false; }
ImPlotFlags pflags = cfg.show_legend ? 0 : ImPlotFlags_NoLegend;
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##line", size, pflags)) return false;
ImPlot::SetupAxes(nullptr, nullptr, axflag(cfg), axflag(cfg));
int n = nums.empty() ? 0 : (int)nums[0].vals.size();
if (n == 0) { ImPlot::EndPlot(); return false; }
// X column: cfg.x_col override; sino primer numeric si hay >=2; sino indices.
int x_idx = -1;
if (!cfg.x_col.empty()) {
int xc = find_header(out, cfg.x_col);
if (xc >= 0 && (out.types[xc] == ColumnType::Int || out.types[xc] == ColumnType::Float)) {
x_idx = xc;
}
}
std::vector<double> idx_xs;
const double* xs = nullptr;
int start_y = 0;
std::vector<double> x_data_external;
if (x_idx >= 0) {
x_data_external = extract_numeric(out, x_idx);
xs = x_data_external.data();
} else if (nums.size() >= 2 && cfg.y_cols.empty()) {
xs = nums[0].vals.data();
start_y = 1;
} else {
idx_xs.resize(n);
for (int i = 0; i < n; ++i) idx_xs[i] = i;
xs = idx_xs.data();
}
bool only_one = (cfg.primary_color != 0) && (nums.size() - start_y == 1);
for (size_t i = (size_t)start_y; i < nums.size(); ++i) {
const auto& nc = nums[i];
ImU32 col = only_one ? (ImU32)cfg.primary_color : 0;
int marker = cfg.show_markers ? ImPlotMarker_Circle : ImPlotMarker_None;
if (mode == ViewMode::Area) {
if (col) {
ImPlot::PlotShaded(nc.name.c_str(), xs, nc.vals.data(), (int)nc.vals.size(), 0.0,
ImPlotSpec(ImPlotProp_FillColor, col, ImPlotProp_LineColor, col));
} else {
ImPlot::PlotShaded(nc.name.c_str(), xs, nc.vals.data(), (int)nc.vals.size(), 0.0);
}
} else if (mode == ViewMode::Stairs) {
if (col) {
ImPlot::PlotStairs(nc.name.c_str(), xs, nc.vals.data(), (int)nc.vals.size(),
ImPlotSpec(ImPlotProp_LineColor, col));
} else {
ImPlot::PlotStairs(nc.name.c_str(), xs, nc.vals.data(), (int)nc.vals.size());
}
} else {
if (col) {
ImPlot::PlotLine(nc.name.c_str(), xs, nc.vals.data(), (int)nc.vals.size(),
ImPlotSpec(ImPlotProp_Flags, ImPlotLineFlags_SkipNaN,
ImPlotProp_LineColor, col,
ImPlotProp_Marker, marker));
} else {
ImPlot::PlotLine(nc.name.c_str(), xs, nc.vals.data(), (int)nc.vals.size(),
ImPlotSpec(ImPlotProp_Flags, ImPlotLineFlags_SkipNaN,
ImPlotProp_Marker, marker));
}
}
}
ImPlot::EndPlot();
return true;
}
bool render_scatter(const StageOutput& out, const ViewConfig& cfg, ImVec2 size,
int* clicked_row_out = nullptr) {
// Soporte cfg.x_col + cfg.y_cols[0]
int xc = find_header(out, cfg.x_col);
int yc = !cfg.y_cols.empty() ? find_header(out, cfg.y_cols[0]) : -1;
std::vector<NumCol> nums;
if (xc >= 0 && yc >= 0) {
NumCol a{xc, out.headers[xc], extract_numeric(out, xc)};
NumCol b{yc, out.headers[yc], extract_numeric(out, yc)};
nums = {a, b};
} else {
nums = collect_numeric(out, 4);
}
if (nums.size() < 2) { info_text("Need 2 numeric columns"); return false; }
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##scatter", size, cfg.show_legend ? 0 : ImPlotFlags_NoLegend)) return false;
ImPlot::SetupAxes(nums[0].name.c_str(), nums[1].name.c_str(),
axflag(cfg), axflag(cfg));
if (cfg.primary_color) {
ImU32 col = (ImU32)cfg.primary_color;
ImPlot::PlotScatter("##s", nums[0].vals.data(), nums[1].vals.data(),
(int)nums[0].vals.size(),
ImPlotSpec(ImPlotProp_MarkerFillColor, col,
ImPlotProp_MarkerLineColor, col));
} else {
ImPlot::PlotScatter("##s", nums[0].vals.data(), nums[1].vals.data(),
(int)nums[0].vals.size());
}
if (clicked_row_out &&
ImPlot::IsPlotHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
ImPlotPoint p = ImPlot::GetPlotMousePos();
int idx = nearest_index_2d(p.x, p.y,
nums[0].vals.data(), nums[1].vals.data(),
(int)nums[0].vals.size());
if (idx >= 0) *clicked_row_out = idx;
}
ImPlot::EndPlot();
return true;
}
bool render_bubble(const StageOutput& out, const ViewConfig& cfg, ImVec2 size,
int* clicked_row_out = nullptr) {
int xc = find_header(out, cfg.x_col);
int yc = !cfg.y_cols.empty() ? find_header(out, cfg.y_cols[0]) : -1;
int sc = resolve_size(out, cfg, -1);
std::vector<NumCol> nums;
if (xc >= 0 && yc >= 0 && sc >= 0) {
nums = {
{xc, out.headers[xc], extract_numeric(out, xc)},
{yc, out.headers[yc], extract_numeric(out, yc)},
{sc, out.headers[sc], extract_numeric(out, sc)},
};
} else {
nums = collect_numeric(out, 4);
}
if (nums.size() < 3) { info_text("Need 3 numeric columns (x, y, size)"); return false; }
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##bubble", size, cfg.show_legend ? 0 : ImPlotFlags_NoLegend)) return false;
ImPlot::SetupAxes(nums[0].name.c_str(), nums[1].name.c_str(),
axflag(cfg), axflag(cfg));
ImPlot::PlotBubbles("##b", nums[0].vals.data(), nums[1].vals.data(),
nums[2].vals.data(), (int)nums[0].vals.size());
if (clicked_row_out &&
ImPlot::IsPlotHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
ImPlotPoint p = ImPlot::GetPlotMousePos();
int idx = nearest_index_2d(p.x, p.y,
nums[0].vals.data(), nums[1].vals.data(),
(int)nums[0].vals.size());
if (idx >= 0) *clicked_row_out = idx;
}
ImPlot::EndPlot();
return true;
}
bool render_histogram(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
auto nums = collect_numeric_filtered(out, cfg, 4);
if (nums.empty()) { info_text("Need 1 numeric column"); return false; }
auto vals = finite(nums[0].vals);
if (vals.empty()) { info_text("No finite values"); return false; }
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##hist", size, cfg.show_legend ? 0 : ImPlotFlags_NoLegend)) return false;
ImPlot::SetupAxes(nums[0].name.c_str(), "count",
axflag(cfg), axflag(cfg));
int bins = (cfg.hist_bins > 0) ? cfg.hist_bins : ImPlotBin_Sturges;
if (cfg.primary_color) {
ImU32 col = (ImU32)cfg.primary_color;
ImPlot::PlotHistogram("##h", vals.data(), (int)vals.size(), bins, 1.0,
ImPlotRange(),
ImPlotSpec(ImPlotProp_FillColor, col,
ImPlotProp_LineColor, col));
} else {
ImPlot::PlotHistogram("##h", vals.data(), (int)vals.size(), bins);
}
ImPlot::EndPlot();
return true;
}
bool render_hist2d(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
int xc = find_header(out, cfg.x_col);
int yc = !cfg.y_cols.empty() ? find_header(out, cfg.y_cols[0]) : -1;
std::vector<NumCol> nums;
if (xc >= 0 && yc >= 0) {
nums = {
{xc, out.headers[xc], extract_numeric(out, xc)},
{yc, out.headers[yc], extract_numeric(out, yc)},
};
} else {
nums = collect_numeric(out, 2);
}
if (nums.size() < 2) { info_text("Need 2 numeric columns"); return false; }
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##hist2d", size, cfg.show_legend ? 0 : ImPlotFlags_NoLegend)) return false;
ImPlot::SetupAxes(nums[0].name.c_str(), nums[1].name.c_str());
int bins = (cfg.hist_bins > 0) ? cfg.hist_bins : ImPlotBin_Sturges;
ImPlot::PlotHistogram2D("##h2", nums[0].vals.data(), nums[1].vals.data(),
(int)nums[0].vals.size(), bins, bins);
ImPlot::EndPlot();
return true;
}
bool render_heatmap(const StageOutput& out, const ViewConfig& cfg, ImVec2 size,
int* clicked_row_out = nullptr) {
auto nums = collect_numeric_filtered(out, cfg, 64);
if (nums.empty()) { info_text("Need numeric columns"); return false; }
int cols = (int)nums.size();
int rows = (int)nums[0].vals.size();
if (rows == 0) { info_text("No rows"); return false; }
std::vector<double> mat((size_t)rows * cols, 0.0);
double mn = +1e300, mx = -1e300;
for (int c = 0; c < cols; ++c) {
for (int r = 0; r < rows; ++r) {
double d = nums[c].vals[r];
if (std::isnan(d)) d = 0;
mat[(size_t)r * cols + c] = d;
if (d < mn) mn = d; if (d > mx) mx = d;
}
}
if (mn == mx) { mx = mn + 1; }
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##heatmap", size, 0)) return false;
ImPlot::PlotHeatmap("##hm", mat.data(), rows, cols, mn, mx, nullptr);
if (clicked_row_out &&
ImPlot::IsPlotHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
ImPlotPoint p = ImPlot::GetPlotMousePos();
// ImPlot heatmap Y se pinta de top a bottom; plot mouse_y va igual
// (default scale 0..rows). Mapeo directo.
int rr, cc;
heatmap_cell_at(p.x, p.y, rows, cols, rr, cc);
if (rr >= 0) *clicked_row_out = rr;
(void)cc;
}
ImPlot::EndPlot();
return true;
}
bool render_pie(const StageOutput& out, const ViewConfig& cfg, bool donut, ImVec2 size,
int* clicked_row_out = nullptr) {
int cat = resolve_cat(out, cfg, first_category_col(out));
auto nums = collect_numeric_filtered(out, cfg, 1);
if (cat < 0 || nums.empty()) { info_text("Need 1 category + 1 numeric"); return false; }
auto cats = extract_category(out, cat);
int n = std::min((int)cats.size(), (int)nums[0].vals.size());
if (n == 0) return false;
std::vector<double> values(n);
std::vector<const char*> labels(n);
for (int i = 0; i < n; ++i) {
double d = nums[0].vals[i];
values[i] = std::isnan(d) ? 0.0 : std::abs(d);
labels[i] = cats[i].c_str();
}
ImPlotFlags pf = ImPlotFlags_Equal;
if (!cfg.show_legend) pf |= ImPlotFlags_NoLegend;
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##pie", size, pf)) return false;
ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations,
ImPlotAxisFlags_NoDecorations);
ImPlot::SetupAxesLimits(0, 1, 0, 1, ImPlotCond_Always);
double radius = (cfg.pie_radius > 0) ? cfg.pie_radius : (donut ? 0.4 : 0.45);
ImPlot::PlotPieChart(labels.data(), values.data(), n, 0.5, 0.5, radius, "%.1f");
if (donut) {
// Draw inner hole as solid circle by overlaying a smaller pie of one slice transparent.
// Simpler: just visually it's a circle with text. Use no extra primitive for now.
}
if (clicked_row_out &&
ImPlot::IsPlotHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
ImPlotPoint p = ImPlot::GetPlotMousePos();
double dx = p.x - 0.5, dy = p.y - 0.5;
double dist2 = dx*dx + dy*dy;
double inner = donut ? (radius * 0.5) : 0.0;
if (dist2 <= radius * radius && dist2 >= inner * inner) {
double ang = pie_angle(0.5, 0.5, p.x, p.y);
int idx = pie_slice_at_angle(ang, values.data(), n);
if (idx >= 0) *clicked_row_out = idx;
}
}
ImPlot::EndPlot();
return true;
}
bool render_funnel(const StageOutput& out, const ViewConfig& cfg, ImVec2 size,
int* clicked_row_out = nullptr) {
int cat = resolve_cat(out, cfg, first_category_col(out));
auto nums = collect_numeric_filtered(out, cfg, 1);
if (cat < 0 || nums.empty()) { info_text("Need 1 category + 1 numeric"); return false; }
auto cats = extract_category(out, cat);
int n = std::min((int)cats.size(), (int)nums[0].vals.size());
if (n == 0) return false;
// Sort desc by value
std::vector<int> idx(n);
for (int i = 0; i < n; ++i) idx[i] = i;
std::sort(idx.begin(), idx.end(), [&](int a, int b) {
double da = std::isnan(nums[0].vals[a]) ? -1e300 : nums[0].vals[a];
double db = std::isnan(nums[0].vals[b]) ? -1e300 : nums[0].vals[b];
return da > db;
});
std::vector<double> ys(n);
std::vector<double> ticks(n);
std::vector<const char*> labels(n);
std::vector<std::string> labels_store(n);
for (int i = 0; i < n; ++i) {
double d = nums[0].vals[idx[i]];
ys[i] = std::isnan(d) ? 0 : d;
ticks[i] = n - 1 - i; // descending order
labels_store[i] = cats[idx[i]];
labels[i] = labels_store[i].c_str();
}
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##funnel", size, 0)) return false;
ImPlot::SetupAxes(nums[0].name.c_str(), out.headers[cat].c_str(),
axflag(cfg), axflag(cfg));
ImPlot::SetupAxisTicks(ImAxis_Y1, ticks.data(), n, labels.data(), false);
ImPlot::PlotBars(nums[0].name.c_str(), ys.data(), ticks.data(), n, 0.85,
ImPlotSpec(ImPlotProp_Flags, ImPlotBarsFlags_Horizontal));
if (clicked_row_out &&
ImPlot::IsPlotHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
ImPlotPoint p = ImPlot::GetPlotMousePos();
int tick_idx = (int)(p.y + 0.5);
// ticks[i] = n-1-i. Invertir para idx en orden sorted descendiente.
int sorted_pos = (n - 1) - tick_idx;
if (sorted_pos >= 0 && sorted_pos < n) {
// idx[sorted_pos] da indice de row original en out.
*clicked_row_out = idx[sorted_pos];
}
}
ImPlot::EndPlot();
return true;
}
bool render_waterfall(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
auto nums = collect_numeric_filtered(out, cfg, 1);
if (nums.empty()) { info_text("Need 1 numeric column"); return false; }
int n = (int)nums[0].vals.size();
if (n == 0) return false;
int cat = resolve_cat(out, cfg, first_category_col(out));
auto cats = (cat >= 0) ? extract_category(out, cat) : std::vector<std::string>();
std::vector<double> running(n + 1, 0);
for (int i = 0; i < n; ++i) {
double d = std::isnan(nums[0].vals[i]) ? 0 : nums[0].vals[i];
running[i + 1] = running[i] + d;
}
std::vector<double> ticks(n);
for (int i = 0; i < n; ++i) ticks[i] = i;
std::vector<const char*> labels(n);
for (int i = 0; i < n; ++i) labels[i] = (i < (int)cats.size()) ? cats[i].c_str() : "";
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##waterfall", size, 0)) return false;
ImPlot::SetupAxes(nullptr, nums[0].name.c_str(),
axflag(cfg), axflag(cfg));
if (cat >= 0) ImPlot::SetupAxisTicks(ImAxis_X1, ticks.data(), n, labels.data(), false);
// Draw stems with rectangles via error-bars trick: low=cum_prev, high=cum_curr.
std::vector<double> mid(n), err(n);
for (int i = 0; i < n; ++i) {
mid[i] = (running[i] + running[i + 1]) * 0.5;
err[i] = std::abs((running[i + 1] - running[i]) * 0.5);
}
ImPlot::PlotErrorBars("##wf", ticks.data(), mid.data(), err.data(), n);
ImPlot::PlotLine("cum", running.data() + 1, n);
ImPlot::EndPlot();
return true;
}
bool render_kpi_single(const StageOutput& out, const ViewConfig& cfg) {
int nc = !cfg.y_cols.empty() ? find_header(out, cfg.y_cols[0]) : -1;
if (nc < 0) nc = first_numeric_col(out);
if (nc < 0) { info_text("Need 1 numeric column"); return false; }
auto vals = extract_numeric(out, nc);
if (vals.empty()) { info_text("Empty"); return false; }
double last = std::nan("");
for (auto v : vals) if (!std::isnan(v)) last = v;
if (std::isnan(last)) { info_text("No finite values"); return false; }
char buf[64];
if (std::abs(last) >= 1e6) std::snprintf(buf, sizeof(buf), "%.2fM", last / 1e6);
else if (std::abs(last) >= 1e3) std::snprintf(buf, sizeof(buf), "%.2fK", last / 1e3);
else std::snprintf(buf, sizeof(buf), "%.3g", last);
ImVec2 avail = ImGui::GetContentRegionAvail();
ImGui::SetWindowFontScale(4.0f);
ImVec2 sz = ImGui::CalcTextSize(buf);
ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + (avail.x - sz.x) * 0.5f,
ImGui::GetCursorPosY() + (avail.y - sz.y) * 0.5f - 20));
ImGui::TextUnformatted(buf);
ImGui::SetWindowFontScale(1.0f);
sz = ImGui::CalcTextSize(out.headers[nc].c_str());
ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + (avail.x - sz.x) * 0.5f,
ImGui::GetCursorPosY() + (avail.y - sz.y) * 0.5f - 10));
ImGui::TextDisabled("%s", out.headers[nc].c_str());
return true;
}
bool render_kpi_grid(const StageOutput& out, const ViewConfig& cfg) {
auto nums = collect_numeric_filtered(out, cfg, 12);
if (nums.empty()) { info_text("Need numeric columns"); return false; }
ImVec2 avail = ImGui::GetContentRegionAvail();
int per_row = std::max(1, (int)(avail.x / 220));
int idx = 0;
for (auto& nc : nums) {
double last = std::nan("");
for (auto v : nc.vals) if (!std::isnan(v)) last = v;
if (std::isnan(last)) last = 0;
char buf[64];
if (std::abs(last) >= 1e6) std::snprintf(buf, sizeof(buf), "%.2fM", last / 1e6);
else if (std::abs(last) >= 1e3) std::snprintf(buf, sizeof(buf), "%.2fK", last / 1e3);
else std::snprintf(buf, sizeof(buf), "%.4g", last);
ImGui::BeginChild((ImGuiID)(0x1000 + idx), ImVec2(210, 100), true);
ImGui::TextDisabled("%s", nc.name.c_str());
ImGui::SetWindowFontScale(2.4f);
ImGui::TextUnformatted(buf);
ImGui::SetWindowFontScale(1.0f);
ImGui::EndChild();
if ((idx % per_row) != (per_row - 1)) ImGui::SameLine();
idx++;
}
return true;
}
bool render_stem(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
auto nums = collect_numeric_filtered(out, cfg, 1);
if (nums.empty()) { info_text("Need 1 numeric column"); return false; }
int n = (int)nums[0].vals.size();
std::vector<double> xs(n); for (int i = 0; i < n; ++i) xs[i] = i;
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##stem", size, 0)) return false;
ImPlot::SetupAxes(nullptr, nums[0].name.c_str(),
axflag(cfg), axflag(cfg));
ImPlot::PlotStems(nums[0].name.c_str(), xs.data(), nums[0].vals.data(), n);
ImPlot::EndPlot();
return true;
}
bool render_errorbars(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
auto nums = collect_numeric_filtered(out, cfg, 4);
if (nums.size() < 2) { info_text("Need 2 numeric columns (value, err)"); return false; }
int n = (int)nums[0].vals.size();
std::vector<double> xs(n); for (int i = 0; i < n; ++i) xs[i] = i;
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##eb", size, 0)) return false;
ImPlot::SetupAxes(nullptr, nums[0].name.c_str(),
axflag(cfg), axflag(cfg));
ImPlot::PlotErrorBars(nums[0].name.c_str(), xs.data(),
nums[0].vals.data(), nums[1].vals.data(), n);
ImPlot::PlotScatter("##s", xs.data(), nums[0].vals.data(), n);
ImPlot::EndPlot();
return true;
}
// BoxPlot: agrupar por categoria, calcular min/p25/p50/p75/max y dibujar
// rectangulos manuales via PlotShaded + lineas.
bool render_boxplot(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
int cat = resolve_cat(out, cfg, first_category_col(out));
auto nums = collect_numeric_filtered(out, cfg, 1);
if (cat < 0 || nums.empty()) { info_text("Need 1 category + 1 numeric"); return false; }
auto cats = extract_category(out, cat);
int n = std::min((int)cats.size(), (int)nums[0].vals.size());
if (n == 0) return false;
// Group values by category
std::unordered_map<std::string, std::vector<double>> groups;
std::vector<std::string> order;
for (int i = 0; i < n; ++i) {
if (groups.find(cats[i]) == groups.end()) order.push_back(cats[i]);
double d = nums[0].vals[i];
if (!std::isnan(d)) groups[cats[i]].push_back(d);
}
int G = (int)order.size();
if (G == 0) return false;
std::vector<double> mn(G), p25(G), p50(G), p75(G), mx(G), xs(G);
std::vector<const char*> labels(G);
for (int g = 0; g < G; ++g) {
auto& v = groups[order[g]];
std::sort(v.begin(), v.end());
int N = (int)v.size();
xs[g] = g;
labels[g]= order[g].c_str();
if (N == 0) { mn[g]=p25[g]=p50[g]=p75[g]=mx[g]=0; continue; }
mn[g] = v.front();
mx[g] = v.back();
p25[g] = v[std::min(N - 1, (int)(N * 0.25))];
p50[g] = v[std::min(N - 1, (int)(N * 0.50))];
p75[g] = v[std::min(N - 1, (int)(N * 0.75))];
}
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##box", size, 0)) return false;
ImPlot::SetupAxes(out.headers[cat].c_str(), nums[0].name.c_str(),
axflag(cfg), axflag(cfg));
ImPlot::SetupAxisTicks(ImAxis_X1, xs.data(), G, labels.data(), false);
// Whiskers: stems from min to max
for (int g = 0; g < G; ++g) {
double lo[2] = { mn[g], mx[g] };
double xx[2] = { xs[g], xs[g] };
ImPlot::PlotLine("##wh", xx, lo, 2);
}
// Box: p25..p75 as bars centered on p50
std::vector<double> mid(G), half(G);
for (int g = 0; g < G; ++g) {
mid[g] = (p25[g] + p75[g]) * 0.5;
half[g] = (p75[g] - p25[g]) * 0.5;
}
ImPlot::PlotErrorBars("box", xs.data(), mid.data(), half.data(), G);
ImPlot::PlotScatter("median", xs.data(), p50.data(), G);
ImPlot::EndPlot();
return true;
}
// Candlestick: tiempo + O/H/L/C. Asume 4 primeras cols numericas en ese orden.
bool render_candlestick(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
auto nums = collect_numeric_filtered(out, cfg, 8);
if (nums.size() < 4) { info_text("Need 4 numeric columns: O/H/L/C"); return false; }
int n = (int)nums[0].vals.size();
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##candle", size, 0)) return false;
ImPlot::SetupAxes("t", "price", axflag(cfg), axflag(cfg));
std::vector<double> xs(n); for (int i = 0; i < n; ++i) xs[i] = i;
const auto& O = nums[0].vals;
const auto& H = nums[1].vals;
const auto& L = nums[2].vals;
const auto& C = nums[3].vals;
// Wicks
for (int i = 0; i < n; ++i) {
double xx[2] = { xs[i], xs[i] };
double yy[2] = { L[i], H[i] };
ImPlot::PlotLine("##wick", xx, yy, 2);
}
// Body via PlotBars(mid, |C-O|)? Simpler: separate lines.
std::vector<double> body_low(n), body_high(n), body_mid(n), body_err(n);
for (int i = 0; i < n; ++i) {
body_low[i] = std::min(O[i], C[i]);
body_high[i] = std::max(O[i], C[i]);
body_mid[i] = (body_low[i] + body_high[i]) * 0.5;
body_err[i] = (body_high[i] - body_low[i]) * 0.5;
}
ImPlot::PlotErrorBars("OHLC", xs.data(), body_mid.data(), body_err.data(), n);
ImPlot::EndPlot();
return true;
}
bool render_radar(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
auto nums = collect_numeric_filtered(out, cfg, 12);
if (nums.size() < 3) { info_text("Need 3+ numeric columns"); return false; }
int K = (int)nums.size();
int n = (int)nums[0].vals.size();
if (n == 0) return false;
// Take first row as the polygon.
std::vector<double> xs(K + 1), ys(K + 1);
double radius_norm = 0;
for (int k = 0; k < K; ++k) {
double d = nums[k].vals[0];
if (std::isnan(d)) d = 0;
radius_norm = std::max(radius_norm, std::abs(d));
}
if (radius_norm == 0) radius_norm = 1;
for (int k = 0; k < K; ++k) {
double v = nums[k].vals[0]; if (std::isnan(v)) v = 0;
double angle = 2 * 3.14159265358979 * k / K - 3.14159265358979 / 2;
double r = v / radius_norm;
xs[k] = std::cos(angle) * r;
ys[k] = std::sin(angle) * r;
}
xs[K] = xs[0]; ys[K] = ys[0];
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##radar", size,
ImPlotFlags_Equal | ImPlotFlags_NoLegend)) return false;
ImPlot::SetupAxesLimits(-1.2, 1.2, -1.2, 1.2, ImPlotCond_Always);
ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations,
ImPlotAxisFlags_NoDecorations);
// Grid rings
for (double rr : {0.25, 0.5, 0.75, 1.0}) {
double gx[64], gy[64];
for (int i = 0; i < 64; ++i) {
double a = 2 * 3.14159265358979 * i / 63;
gx[i] = std::cos(a) * rr; gy[i] = std::sin(a) * rr;
}
ImPlot::PlotLine("##grid", gx, gy, 64);
}
ImPlot::PlotLine("radar", xs.data(), ys.data(), K + 1);
// Axis labels
for (int k = 0; k < K; ++k) {
double a = 2 * 3.14159265358979 * k / K - 3.14159265358979 / 2;
ImPlot::PlotText(nums[k].name.c_str(), std::cos(a) * 1.1, std::sin(a) * 1.1);
}
ImPlot::EndPlot();
return true;
}
} // anon
bool render(const StageOutput& out, ViewMode mode,
const ViewConfig& cfg, ImVec2 size,
int* clicked_row_out) {
if (clicked_row_out) *clicked_row_out = -1;
if (out.rows == 0 || out.cols == 0) {
info_text("No data");
return false;
}
switch (mode) {
case ViewMode::Table: return false;
case ViewMode::Bar:
case ViewMode::Column:
case ViewMode::GroupedBar:
case ViewMode::StackedBar: return render_bar_like(out, mode, cfg, size, clicked_row_out);
case ViewMode::Line:
case ViewMode::Area:
case ViewMode::Stairs: return render_line_like(out, mode, cfg, size);
case ViewMode::Scatter: return render_scatter(out, cfg, size, clicked_row_out);
case ViewMode::Bubble: return render_bubble(out, cfg, size, clicked_row_out);
case ViewMode::Histogram: return render_histogram(out, cfg, size);
case ViewMode::Histogram2D: return render_hist2d(out, cfg, size);
case ViewMode::Heatmap: return render_heatmap(out, cfg, size, clicked_row_out);
case ViewMode::BoxPlot: return render_boxplot(out, cfg, size);
case ViewMode::Stem: return render_stem(out, cfg, size);
case ViewMode::ErrorBars: return render_errorbars(out, cfg, size);
case ViewMode::Pie: return render_pie(out, cfg, false, size, clicked_row_out);
case ViewMode::Donut: return render_pie(out, cfg, true, size, clicked_row_out);
case ViewMode::Funnel: return render_funnel(out, cfg, size, clicked_row_out);
case ViewMode::Waterfall: return render_waterfall(out, cfg, size);
case ViewMode::KPI: return render_kpi_single(out, cfg);
case ViewMode::KPIGrid: return render_kpi_grid(out, cfg);
case ViewMode::Candlestick: return render_candlestick(out, cfg, size);
case ViewMode::Radar: return render_radar(out, cfg, size);
}
return false;
}
} // namespace viz