7eb7b3d0c8
Snapshot de WIP acumulado de sesiones previas antes de merge wave 1 del flow 0008 (kanban_cpp + agent_runner_api + DoD schema). Incluye: - dev/flows/0008-kanban-cpp-and-agent-workflows.md - dev/issues/0112-0119*.md (7 sub-issues) - WIP previo en cmd/fn/doctor.go, registry/*, modules/, cpp/, etc. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
430 lines
17 KiB
C++
430 lines
17 KiB
C++
// data_table_viz_panels — paneles de visualizacion lateral de la tabla TQL.
|
|
// Sub-funcion extraida de modules/data_table/data_table.cpp (issue 0107c).
|
|
//
|
|
// Funciones implementadas:
|
|
// - draw_table_toggle (ex lineas 745-767)
|
|
// - draw_extra_panel (ex lineas 771-856)
|
|
// - draw_viz_config_popup (ex lineas 858-1021)
|
|
// - draw_viz_selector (ex lineas 1034-1110)
|
|
// - maybe_recompute_stats (ex lineas 1118-1145)
|
|
|
|
#include "viz/data_table_viz_panels.h"
|
|
#include "data_table/data_table_internal.h"
|
|
#include "viz/data_table_grid.h" // draw_cell_custom
|
|
#include "core/data_table_types.h"
|
|
#include "core/compute_column_stats.h"
|
|
#include "core/tql_emit.h"
|
|
#include "viz/viz_render.h"
|
|
#include "imgui.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace data_table {
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// draw_table_toggle
|
|
// ---------------------------------------------------------------------------
|
|
void draw_table_toggle(ViewMode& display, ViewMode& last_non_table,
|
|
const char* id_suffix,
|
|
State* st_opt)
|
|
{
|
|
bool is_table = (display == ViewMode::Table);
|
|
char b[64];
|
|
std::snprintf(b, sizeof(b), "%s##tbl_%s",
|
|
is_table ? "Show chart" : "Show table", id_suffix);
|
|
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(80, 140, 200, 240));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(100, 160, 220, 240));
|
|
if (ImGui::SmallButton(b)) {
|
|
if (is_table) {
|
|
ViewMode tgt = (last_non_table == ViewMode::Table)
|
|
? ViewMode::Bar : last_non_table;
|
|
display = tgt;
|
|
if (st_opt && view_mode_needs_aggregation(tgt)) {
|
|
auto_promote_aggregated(*st_opt);
|
|
}
|
|
} else {
|
|
last_non_table = display;
|
|
display = ViewMode::Table;
|
|
}
|
|
}
|
|
ImGui::PopStyleColor(2);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// draw_extra_panel
|
|
// ---------------------------------------------------------------------------
|
|
bool draw_extra_panel(State& st, VizPanel& p, int idx,
|
|
const StageOutput& so,
|
|
const std::vector<ColumnSpec>* col_specs)
|
|
{
|
|
bool close_req = false;
|
|
char child_id[64]; std::snprintf(child_id, sizeof(child_id), "##extra_viz_%d", idx);
|
|
ImGui::BeginChild(child_id, ImVec2(0, 320), true);
|
|
|
|
// Toolbar
|
|
int n_modes = 0;
|
|
const ViewMode* modes = all_view_modes(&n_modes);
|
|
ImGui::TextDisabled("View:");
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(180);
|
|
char combo_id[64]; std::snprintf(combo_id, sizeof(combo_id), "##ev_mode_%d", idx);
|
|
if (ImGui::BeginCombo(combo_id, view_mode_label(p.display))) {
|
|
for (int i = 0; i < n_modes; ++i) {
|
|
bool sel = (modes[i] == p.display);
|
|
if (ImGui::Selectable(view_mode_label(modes[i]), sel)) {
|
|
p.display = modes[i];
|
|
p.config.fit_request = true;
|
|
}
|
|
if (sel) ImGui::SetItemDefaultFocus();
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
ImGui::SameLine();
|
|
char fit_id[32]; std::snprintf(fit_id, sizeof(fit_id), "Fit##ev_fit_%d", idx);
|
|
if (ImGui::SmallButton(fit_id)) p.config.fit_request = true;
|
|
ImGui::SameLine();
|
|
char lock_id[32]; std::snprintf(lock_id, sizeof(lock_id), "%s##ev_lock_%d",
|
|
p.config.locked ? "Locked" : "Lock", idx);
|
|
if (p.config.locked) {
|
|
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(180, 60, 60, 230));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(200, 80, 80, 240));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(150, 40, 40, 240));
|
|
}
|
|
if (ImGui::SmallButton(lock_id)) p.config.locked = !p.config.locked;
|
|
if (p.config.locked) ImGui::PopStyleColor(3);
|
|
ImGui::SameLine();
|
|
char close_id[32]; std::snprintf(close_id, sizeof(close_id), "X##ev_close_%d", idx);
|
|
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(120, 50, 50, 220));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(160, 70, 70, 240));
|
|
if (ImGui::SmallButton(close_id)) close_req = true;
|
|
ImGui::PopStyleColor(2);
|
|
|
|
// Toggle Table <-> View per-panel
|
|
char ts[32]; std::snprintf(ts, sizeof(ts), "ep%d", idx);
|
|
draw_table_toggle(p.display, p.last_non_table, ts);
|
|
|
|
// Render: si Table -> mini table; else chart.
|
|
if (p.display == ViewMode::Table) {
|
|
ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
|
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY |
|
|
ImGuiTableFlags_ScrollX;
|
|
char tid[64]; std::snprintf(tid, sizeof(tid), "##ep_table_%d", idx);
|
|
if (so.cols > 0 && ImGui::BeginTable(tid, so.cols, flags, ImVec2(0, 0))) {
|
|
for (int c = 0; c < so.cols; ++c)
|
|
ImGui::TableSetupColumn(so.headers[c].c_str());
|
|
ImGui::TableHeadersRow();
|
|
for (int r = 0; r < so.rows; ++r) {
|
|
ImGui::TableNextRow();
|
|
for (int c = 0; c < so.cols; ++c) {
|
|
ImGui::TableSetColumnIndex(c);
|
|
const char* s = so.cells[(size_t)r * so.cols + c];
|
|
// Issue 0081-N: declarative renderer for extra panel mini-table.
|
|
// events_out not propagated to mini-table (secondary render).
|
|
bool custom_ep = false;
|
|
if (col_specs && c < (int)col_specs->size()) {
|
|
const ColumnSpec& cs = (*col_specs)[(size_t)c];
|
|
if (cs.renderer != CellRenderer::Text) {
|
|
draw_cell_custom(cs, s, r, c, nullptr);
|
|
custom_ep = true;
|
|
}
|
|
}
|
|
if (!custom_ep) ImGui::TextUnformatted(s ? s : "");
|
|
}
|
|
}
|
|
ImGui::EndTable();
|
|
}
|
|
} else {
|
|
viz::render(so, p.display, p.config, ImVec2(-1, -1));
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
(void)st;
|
|
return close_req;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// draw_viz_config_popup
|
|
// ---------------------------------------------------------------------------
|
|
void draw_viz_config_popup(State& st) {
|
|
if (!ImGui::BeginPopup("##viz_cfg_popup")) return;
|
|
ImGui::Text("Configure: %s", view_mode_label(st.display));
|
|
ImGui::Separator();
|
|
|
|
auto cols = collect_active_col_info(st);
|
|
std::vector<const char*> all_names;
|
|
std::vector<const char*> num_names;
|
|
std::vector<const char*> cat_names;
|
|
for (auto& c : cols) {
|
|
all_names.push_back(c.name.c_str());
|
|
if (c.type == ColumnType::Int || c.type == ColumnType::Float)
|
|
num_names.push_back(c.name.c_str());
|
|
else
|
|
cat_names.push_back(c.name.c_str());
|
|
}
|
|
|
|
auto& vc = st.viz_config;
|
|
ViewMode m = st.display;
|
|
|
|
auto combo_for_col = [&](const char* label, std::string& target,
|
|
const std::vector<const char*>& options) {
|
|
const char* preview = target.empty() ? "(auto)" : target.c_str();
|
|
ImGui::SetNextItemWidth(220);
|
|
if (ImGui::BeginCombo(label, preview)) {
|
|
if (ImGui::Selectable("(auto)", target.empty())) target.clear();
|
|
for (auto& o : options) {
|
|
bool sel = (target == o);
|
|
if (ImGui::Selectable(o, sel)) target = o;
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
};
|
|
|
|
// X col: scatter, line, area, stairs, hist2d, bubble
|
|
bool needs_x = (m == ViewMode::Scatter || m == ViewMode::Line ||
|
|
m == ViewMode::Area || m == ViewMode::Stairs ||
|
|
m == ViewMode::Histogram2D || m == ViewMode::Bubble);
|
|
if (needs_x) combo_for_col("X column", vc.x_col, num_names);
|
|
|
|
// Y cols: most modes
|
|
bool needs_y = (m != ViewMode::Pie && m != ViewMode::Donut && m != ViewMode::Funnel &&
|
|
m != ViewMode::Candlestick);
|
|
if (needs_y) {
|
|
ImGui::Text("Y columns:");
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("(%d selected; empty = auto)", (int)vc.y_cols.size());
|
|
ImGui::Indent();
|
|
for (auto& nn : num_names) {
|
|
std::string ns = nn;
|
|
bool checked = std::find(vc.y_cols.begin(), vc.y_cols.end(), ns) != vc.y_cols.end();
|
|
if (ImGui::Checkbox(nn, &checked)) {
|
|
if (checked) vc.y_cols.push_back(ns);
|
|
else {
|
|
auto it = std::find(vc.y_cols.begin(), vc.y_cols.end(), ns);
|
|
if (it != vc.y_cols.end()) vc.y_cols.erase(it);
|
|
}
|
|
}
|
|
}
|
|
ImGui::Unindent();
|
|
if (ImGui::SmallButton("Clear Y##clr_y")) vc.y_cols.clear();
|
|
}
|
|
|
|
// Cat col: bar/pie/funnel/box/waterfall
|
|
bool needs_cat = (m == ViewMode::Bar || m == ViewMode::Column ||
|
|
m == ViewMode::GroupedBar || m == ViewMode::StackedBar ||
|
|
m == ViewMode::Pie || m == ViewMode::Donut ||
|
|
m == ViewMode::Funnel || m == ViewMode::BoxPlot ||
|
|
m == ViewMode::Waterfall);
|
|
if (needs_cat) {
|
|
// Si el active stage YA esta agrupado (breakouts != empty), la categoria
|
|
// del chart la dicta el breakout. Mostrar todas las cols del INPUT del
|
|
// stage (= cols pre-agrupacion). Selecionar otra = reemplaza breakouts[0]
|
|
// (re-agrupa).
|
|
int as = st.active_stage;
|
|
bool grouped = (as >= 0 && as < (int)st.stages.size() &&
|
|
!st.stages[as].breakouts.empty());
|
|
const auto& U = ui();
|
|
if (grouped) {
|
|
std::vector<const char*> input_cat_names;
|
|
for (size_t i = 0; i < U.input_headers_active.size() &&
|
|
i < U.input_types_active.size(); ++i) {
|
|
ColumnType t = U.input_types_active[i];
|
|
if (t == ColumnType::String || t == ColumnType::Date ||
|
|
t == ColumnType::Bool || t == ColumnType::Json) {
|
|
input_cat_names.push_back(U.input_headers_active[i].c_str());
|
|
}
|
|
}
|
|
std::string cur_break = st.stages[as].breakouts[0];
|
|
const char* preview = cur_break.empty() ? "(none)" : cur_break.c_str();
|
|
ImGui::SetNextItemWidth(220);
|
|
if (ImGui::BeginCombo("Category (breakout)", preview)) {
|
|
for (auto& o : input_cat_names) {
|
|
bool sel = (cur_break == o);
|
|
if (ImGui::Selectable(o, sel)) {
|
|
st.stages[as].breakouts[0] = o;
|
|
}
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
} else {
|
|
combo_for_col("Category", vc.cat_col, cat_names);
|
|
}
|
|
}
|
|
|
|
// Size col: bubble
|
|
if (m == ViewMode::Bubble) combo_for_col("Size column", vc.size_col, num_names);
|
|
|
|
// Color
|
|
ImGui::Separator();
|
|
float col_f[4] = {
|
|
((vc.primary_color) & 0xFF) / 255.0f,
|
|
((vc.primary_color >> 8) & 0xFF) / 255.0f,
|
|
((vc.primary_color >> 16) & 0xFF) / 255.0f,
|
|
((vc.primary_color >> 24) & 0xFF) / 255.0f,
|
|
};
|
|
if (vc.primary_color == 0) { col_f[0]=col_f[1]=col_f[2]=1.0f; col_f[3]=1.0f; }
|
|
if (ImGui::ColorEdit4("Primary color", col_f, ImGuiColorEditFlags_AlphaBar)) {
|
|
unsigned int r2 = (unsigned int)(col_f[0] * 255);
|
|
unsigned int g2 = (unsigned int)(col_f[1] * 255);
|
|
unsigned int b2 = (unsigned int)(col_f[2] * 255);
|
|
unsigned int a2 = (unsigned int)(col_f[3] * 255);
|
|
vc.primary_color = (a2 << 24) | (b2 << 16) | (g2 << 8) | r2;
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::SmallButton("Auto##color")) vc.primary_color = 0;
|
|
|
|
// Hist bins
|
|
if (m == ViewMode::Histogram || m == ViewMode::Histogram2D) {
|
|
ImGui::SetNextItemWidth(120);
|
|
int bins = vc.hist_bins;
|
|
if (ImGui::InputInt("Bins (0=auto)", &bins)) {
|
|
if (bins < 0) bins = 0;
|
|
vc.hist_bins = bins;
|
|
}
|
|
}
|
|
|
|
// Pie radius
|
|
if (m == ViewMode::Pie || m == ViewMode::Donut) {
|
|
ImGui::SetNextItemWidth(120);
|
|
float rad = vc.pie_radius;
|
|
if (ImGui::SliderFloat("Radius (0=auto)", &rad, 0.0f, 0.5f, "%.2f")) {
|
|
vc.pie_radius = rad;
|
|
}
|
|
}
|
|
|
|
// Toggles
|
|
ImGui::Separator();
|
|
ImGui::Checkbox("Show legend", &vc.show_legend);
|
|
if (m == ViewMode::Line || m == ViewMode::Area || m == ViewMode::Stairs) {
|
|
ImGui::SameLine();
|
|
ImGui::Checkbox("Show markers", &vc.show_markers);
|
|
}
|
|
|
|
ImGui::Separator();
|
|
if (ImGui::SmallButton("Reset config")) {
|
|
vc = ViewConfig{};
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::SmallButton("Close")) ImGui::CloseCurrentPopup();
|
|
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// draw_viz_selector
|
|
// ---------------------------------------------------------------------------
|
|
void draw_viz_selector(State& st) {
|
|
int n_modes = 0;
|
|
const ViewMode* modes = all_view_modes(&n_modes);
|
|
|
|
// Right-align: reserve "View: [combo] [Fit] [Lock] [Config] [Ask AI] [+ Viz]"
|
|
const float combo_w = 200.0f;
|
|
const float total_w = combo_w + 50.0f + 280.0f;
|
|
float right_edge = ImGui::GetWindowContentRegionMax().x;
|
|
float target_x = right_edge - total_w;
|
|
float min_x = ImGui::GetCursorPosX() + 20.0f; // do not overlap breadcrumb
|
|
if (target_x < min_x) target_x = min_x;
|
|
ImGui::SameLine();
|
|
ImGui::SetCursorPosX(target_x);
|
|
|
|
ImGui::TextDisabled("View:");
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(combo_w);
|
|
if (ImGui::BeginCombo("##viz_mode", view_mode_label(st.display))) {
|
|
for (int i = 0; i < n_modes; ++i) {
|
|
bool sel = (modes[i] == st.display);
|
|
if (ImGui::Selectable(view_mode_label(modes[i]), sel)) {
|
|
ViewMode nm = modes[i];
|
|
if (nm != st.display) {
|
|
st.display = nm;
|
|
if (view_mode_needs_aggregation(nm)) {
|
|
auto_promote_aggregated(st);
|
|
}
|
|
}
|
|
}
|
|
if (sel) ImGui::SetItemDefaultFocus();
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::SmallButton("Fit##viz_fit")) {
|
|
st.viz_config.fit_request = true;
|
|
}
|
|
ImGui::SameLine();
|
|
bool locked = st.viz_config.locked;
|
|
if (locked) {
|
|
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(180, 60, 60, 230));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(200, 80, 80, 240));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(150, 40, 40, 240));
|
|
}
|
|
if (ImGui::SmallButton(locked ? "Locked##viz_lock" : "Lock##viz_lock")) {
|
|
st.viz_config.locked = !st.viz_config.locked;
|
|
}
|
|
if (locked) ImGui::PopStyleColor(3);
|
|
ImGui::SameLine();
|
|
if (ImGui::SmallButton("Config##viz_cfg")) {
|
|
ImGui::OpenPopup("##viz_cfg_popup");
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::SmallButton("Ask AI##ask_open")) {
|
|
auto& U2 = ui();
|
|
U2.ask_ai.open = true;
|
|
U2.ask_ai.busy = false;
|
|
U2.ask_ai.error.clear();
|
|
U2.ask_ai.status.clear();
|
|
U2.ask_ai.response_code.clear();
|
|
U2.ask_ai.response_raw.clear();
|
|
U2.ask_ai.current_tql = tql::emit(st,
|
|
std::vector<std::string>(),
|
|
std::vector<ColumnType>());
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::SmallButton("+ Viz##viz_add")) {
|
|
VizPanel p;
|
|
p.display = ViewMode::Bar;
|
|
if (view_mode_needs_aggregation(p.display)) {
|
|
auto_promote_aggregated(st);
|
|
}
|
|
st.extra_panels.push_back(p);
|
|
}
|
|
draw_viz_config_popup(st);
|
|
ImGui::NewLine();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// maybe_recompute_stats
|
|
// ---------------------------------------------------------------------------
|
|
void maybe_recompute_stats(State& st,
|
|
const char* const* cells,
|
|
int row_count, int orig_cols, int eff_cols,
|
|
const std::vector<Filter>& active_filters,
|
|
const std::vector<int>& visible_rows,
|
|
const std::vector<int>& src_for_eff)
|
|
{
|
|
if (!st.stats_mode) return;
|
|
size_t fh = filters_hash(active_filters);
|
|
bool ds_changed = (cells != st.stats_last_cells || row_count != st.stats_last_rows ||
|
|
eff_cols != st.stats_last_eff_cols ||
|
|
(int)st.stats_cache.size() != eff_cols);
|
|
bool fl_changed = (fh != st.stats_last_filter_h ||
|
|
(int)visible_rows.size() != st.stats_last_visible);
|
|
if (!ds_changed && !fl_changed) return;
|
|
st.stats_cache.resize(eff_cols);
|
|
const int* idx = visible_rows.empty() ? nullptr : visible_rows.data();
|
|
int n = (int)visible_rows.size();
|
|
for (int c = 0; c < eff_cols; ++c) {
|
|
int src = src_for_eff[c];
|
|
st.stats_cache[c] = compute_column_stats(cells, row_count, orig_cols, src,
|
|
100000, idx, n);
|
|
}
|
|
st.stats_last_cells = cells;
|
|
st.stats_last_rows = row_count;
|
|
st.stats_last_eff_cols = eff_cols;
|
|
st.stats_last_filter_h = fh;
|
|
st.stats_last_visible = (int)visible_rows.size();
|
|
}
|
|
|
|
} // namespace data_table
|