#include "viz.h" #include "implot.h" #include #include #include #include #include #include #include namespace viz { using data_table::StageOutput; using data_table::ColumnType; using data_table::ViewMode; using data_table::ViewConfig; using data_table::parse_number; using data_table::nearest_index_2d; using data_table::pie_angle; using data_table::pie_slice_at_angle; using data_table::heatmap_cell_at; 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 extract_numeric(const StageOutput& out, int col) { std::vector 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 extract_category(const StageOutput& out, int col) { std::vector 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 vals; }; std::vector collect_numeric(const StageOutput& out, int max_n = 16) { std::vector 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 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 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 finite(const std::vector& v) { std::vector 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 ticks(n); std::vector 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 mat((size_t)items * n, 0.0); std::vector 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 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 idx_xs; const double* xs = nullptr; int start_y = 0; std::vector 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 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 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 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 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 values(n); std::vector 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 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 ys(n); std::vector ticks(n); std::vector labels(n); std::vector 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::vector 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 ticks(n); for (int i = 0; i < n; ++i) ticks[i] = i; std::vector 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 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 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 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> groups; std::vector 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 mn(G), p25(G), p50(G), p75(G), mx(G), xs(G); std::vector 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 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 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 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 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