Files
fn_registry/modules/data_table/data_table.cpp
T

4548 lines
199 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// data_table — render UI completa de tabla TQL.
// Entry-point publica del stack data_table del registry.
// Issue 0081-H. Promovido desde cpp/apps/primitives_gallery/playground/tables/data_table.cpp
//
// Dependencias del registry:
// - core/data_table_types.h (tipos compartidos: State, TableInput, Stage, ...)
// - core/compute_stage.h (compute_stage_cpp_core)
// - core/compute_pipeline.h (compute_pipeline_cpp_core)
// - core/compute_column_stats.h (compute_column_stats_cpp_core)
// - core/auto_detect_type.h (auto_detect_type_cpp_core)
// - core/tql_emit.h (tql_emit_cpp_core)
// - core/tql_apply.h (tql_apply_cpp_core)
// - core/lua_engine.h (lua_engine_cpp_core)
// - core/join_tables.h (join_tables_cpp_core)
// - viz/viz_render.h (viz_render_cpp_viz)
//
// Notas de deuda tecnica:
// - tql_apply_cpp_core expone firma reducida; el playground usaba tql::apply
// con cells/rows/orig_cols. Las llamadas internas de este archivo usan el
// namespace tql:: del playground via include del tql_apply_cpp_core header.
// Pendiente: ampliar tql_apply_cpp_core a la firma extendida (Wave 4/5 proposal).
// - llm_anthropic (Ask AI modal, fase 11): incluido desde el playground (no en registry).
// Pendiente: promover a cpp/functions/infra/llm_anthropic — deuda Wave 4.
// - tql_to_sql (SQL transpile): incluido desde el playground. Pendiente: registry Wave 4.
// - tql_duckdb (FN_TQL_DUCKDB): opcional, sin wrapper en registry todavia.
#include "data_table/data_table.h"
// Framework ImGui (via fn_framework)
#include "imgui.h"
// Registry Wave 1+2 includes (all resolved via fn_table_viz include path).
#include "core/lua_engine.h"
#include "core/tql_apply.h"
#include "core/tql_emit.h"
#include "core/tql_helpers.h"
#include "core/compute_stage.h"
#include "core/compute_pipeline.h"
#include "core/compute_column_stats.h"
#include "core/auto_detect_type.h"
#include "core/join_tables.h"
#include "core/tql_to_sql.h"
#include "viz/viz_render.h"
// llm_anthropic — Ask AI modal. Promoted to registry (cpp/functions/core/) in
// Wave 3.5. Real implementation linked by fn_table_viz; stub kept under
// !FN_LLM_ANTHROPIC for environments that build without the lib.
#ifdef FN_LLM_ANTHROPIC
# include "core/llm_anthropic.h"
#endif
#ifdef FN_TQL_DUCKDB
# include "tql_duckdb.h"
#endif
// fn::local_path — from fn_framework (framework/app_base.h).
// Required by the Ask AI modal and TQL save/load paths.
#include "app_base.h"
#include <algorithm>
#include <cfloat>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <fstream>
#include <string>
#include <unordered_map>
// icons_tabler.h: needed by draw_cell_custom icon renderer (issue 0081-N).
#include "core/icons_tabler.h"
// ---------------------------------------------------------------------------
// llm_anthropic stub (Wave 4 TODO: replace with infra/llm_anthropic.h)
// Provides no-op types/functions so fn_table_viz links without the playground.
// When FN_LLM_ANTHROPIC is defined the real header is included above instead.
// ---------------------------------------------------------------------------
#ifndef FN_LLM_ANTHROPIC
namespace llm_anthropic {
enum class OutputMode { TQL, SQL };
struct AskInput {
std::string question;
std::string tql_current;
std::vector<std::string> col_names;
std::vector<data_table::ColumnType> col_types;
std::vector<std::string> joinable_names;
OutputMode mode = OutputMode::TQL;
std::string model;
int max_tokens = 8192;
};
struct AskResult {
std::string code;
std::string raw;
std::string error;
int tokens_in = 0;
int tokens_out = 0;
};
inline AskResult ask(const AskInput&, const std::string& = "") {
AskResult r;
r.error = "llm_anthropic not available (stub). Build with FN_LLM_ANTHROPIC=1.";
return r;
}
} // namespace llm_anthropic
#endif // FN_LLM_ANTHROPIC
namespace data_table {
// ---------------------------------------------------------------------------
// Helpers from playground data_table_logic — declared static so they do not
// leak into the data_table namespace beyond this translation unit.
// Promoted inline to remove dependency on playground headers. Issue 0081-I.
// ---------------------------------------------------------------------------
// column_type_icon: returns a Tabler icon UTF-8 sequence for each ColumnType.
static const char* column_type_icon(ColumnType t) {
switch (t) {
case ColumnType::Auto: return "\xef\xa4\x9d"; // TI_HELP_CIRCLE
case ColumnType::String: return "\xef\x95\xa7"; // TI_ABC
case ColumnType::Int: return "\xef\x95\x94"; // TI_123
case ColumnType::Float: return "\xef\xa8\xa6"; // TI_DECIMAL
case ColumnType::Bool: return "\xee\xae\xa6"; // TI_CHECKBOX
case ColumnType::Date: return "\xee\xa9\x93"; // TI_CALENDAR
case ColumnType::Json: return "\xee\xaf\x8c"; // TI_BRACES
}
return "?";
}
// ---------------------------------------------------------------------------
// hex_to_imcolor: parses "#rrggbb" or "rrggbb" -> ImVec4 (alpha=1).
// Returns {-1,-1,-1,-1} on failure (caller should skip color push).
// ---------------------------------------------------------------------------
static ImVec4 hex_to_imcolor(const std::string& hex) {
const char* p = hex.c_str();
if (*p == '#') ++p;
unsigned int r = 0, g = 0, b = 0;
if (std::sscanf(p, "%02x%02x%02x", &r, &g, &b) != 3)
return ImVec4(-1.f, -1.f, -1.f, -1.f);
return ImVec4(r / 255.f, g / 255.f, b / 255.f, 1.f);
}
// parse_hex_color: parses "#rrggbb" or "#rrggbbaa" -> ImU32 with explicit alpha.
// Returns IM_COL32(128,128,128,255) on failure (visible fallback).
// v1.4.0 helper for CategoricalChip and ColorScale renderers.
// ---------------------------------------------------------------------------
static ImU32 parse_hex_color(const std::string& hex, float alpha = 1.0f) {
const char* p = hex.c_str();
if (*p == '#') ++p;
unsigned int r = 0, g = 0, b = 0, a = 255;
int parsed = std::sscanf(p, "%02x%02x%02x%02x", &r, &g, &b, &a);
if (parsed < 3) return IM_COL32(128, 128, 128, 255);
if (parsed == 3) {
// alpha parameter overrides when no alpha in hex string
a = (unsigned int)(alpha * 255.f + 0.5f);
}
return IM_COL32(r, g, b, a);
}
// lerp_color_along_stops: LERP between N color stops based on t in [0,1].
// Stops need not be sorted; function sorts a local copy first.
// If stops is empty, uses default green→amber→red gradient.
// alpha overrides the per-channel alpha of the result.
// v1.4.0 helper for ColorScale renderer.
// ---------------------------------------------------------------------------
static ImU32 lerp_color_along_stops(
const std::vector<data_table::ColorStop>& stops, float t, float alpha)
{
// Default green→amber→red when caller provides no stops.
static const std::vector<data_table::ColorStop> kDefault = {
{0.0f, "#22c55e"},
{0.5f, "#f59e0b"},
{1.0f, "#ef4444"},
};
const auto& sv = stops.empty() ? kDefault : stops;
// Sort by position (copy; usually already sorted).
std::vector<data_table::ColorStop> sorted_sv = sv;
std::sort(sorted_sv.begin(), sorted_sv.end(),
[](const data_table::ColorStop& a, const data_table::ColorStop& b){
return a.position < b.position;
});
// Clamp t.
t = t < 0.f ? 0.f : (t > 1.f ? 1.f : t);
// Edge cases: before first stop or after last stop.
if (t <= sorted_sv.front().position)
return parse_hex_color(sorted_sv.front().color, alpha);
if (t >= sorted_sv.back().position)
return parse_hex_color(sorted_sv.back().color, alpha);
// Find surrounding stops.
for (size_t i = 0; i + 1 < sorted_sv.size(); ++i) {
const auto& lo = sorted_sv[i];
const auto& hi = sorted_sv[i + 1];
if (t >= lo.position && t <= hi.position) {
float span = hi.position - lo.position;
float f = (span > 1e-6f) ? (t - lo.position) / span : 0.f;
ImVec4 ca = hex_to_imcolor(lo.color);
ImVec4 cb = hex_to_imcolor(hi.color);
if (ca.x < 0.f) ca = ImVec4(0.5f, 0.5f, 0.5f, 1.f);
if (cb.x < 0.f) cb = ImVec4(0.5f, 0.5f, 0.5f, 1.f);
float r = ca.x + f * (cb.x - ca.x);
float g = ca.y + f * (cb.y - ca.y);
float b = ca.z + f * (cb.z - ca.z);
unsigned int ri = (unsigned int)(r * 255.f + 0.5f);
unsigned int gi = (unsigned int)(g * 255.f + 0.5f);
unsigned int bi = (unsigned int)(b * 255.f + 0.5f);
unsigned int ai = (unsigned int)(alpha * 255.f + 0.5f);
return IM_COL32(ri, gi, bi, ai);
}
}
// Fallback (should not reach here).
return parse_hex_color(sorted_sv.back().color, alpha);
}
// ---------------------------------------------------------------------------
// icon_name_to_glyph: static lookup of icon_name string -> Tabler glyph.
// Covers the ~30 most-used icons. Returns nullptr if not found.
// ---------------------------------------------------------------------------
static const char* icon_name_to_glyph(const std::string& name) {
static const std::unordered_map<std::string, const char*> kMap = {
{"TI_CHECK", TI_CHECK},
{"TI_X", TI_X},
{"TI_ALERT_CIRCLE", TI_ALERT_CIRCLE},
{"TI_CIRCLE_DOT", TI_CIRCLE_DOT},
{"TI_CLOCK", TI_CLOCK},
{"TI_LOADER", TI_LOADER},
{"TI_BAN", TI_BAN},
{"TI_PLAYER_PLAY", TI_PLAYER_PLAY},
{"TI_PLAYER_PAUSE", TI_PLAYER_PAUSE},
{"TI_PLAYER_STOP", TI_PLAYER_STOP},
{"TI_DATABASE", TI_DATABASE},
{"TI_SETTINGS", TI_SETTINGS},
{"TI_USER", TI_USER},
{"TI_USERS", TI_USERS},
{"TI_FILE", TI_FILE},
{"TI_FOLDER", TI_FOLDER},
{"TI_REFRESH", TI_REFRESH},
{"TI_BOLT", TI_BOLT},
{"TI_INFO_CIRCLE", TI_INFO_CIRCLE},
{"TI_ARROW_UP", TI_ARROW_UP},
{"TI_ARROW_DOWN", TI_ARROW_DOWN},
{"TI_ARROW_RIGHT", TI_ARROW_RIGHT},
{"TI_ARROW_LEFT", TI_ARROW_LEFT},
{"TI_DOTS", TI_DOTS},
{"TI_EYE", TI_EYE},
{"TI_EYE_OFF", TI_EYE_OFF},
{"TI_EDIT", TI_EDIT},
{"TI_TRASH", TI_TRASH},
{"TI_COPY", TI_COPY},
{"TI_EXTERNAL_LINK", TI_EXTERNAL_LINK},
};
auto it = kMap.find(name);
return it != kMap.end() ? it->second : nullptr;
}
// ---------------------------------------------------------------------------
// draw_cell_custom: render a cell using the declarative ColumnSpec.
// Called only when spec.renderer != CellRenderer::Text.
// Issue 0081-N, v1.1.0. Phase 2 (v1.2.0): Button renderer + tooltip.
//
// events_out: if non-null and renderer==Button, ButtonClick is pushed on click.
// row_idx / col_idx: logical indices in the TableInput (for event payload).
// ---------------------------------------------------------------------------
static void draw_cell_custom(const ColumnSpec& spec, const char* value,
int row_idx, int col_idx,
std::vector<TableEvent>* events_out) {
if (!value) value = "";
switch (spec.renderer) {
case CellRenderer::Badge: {
// Find a matching badge rule (exact match, case-sensitive).
const BadgeRule* matched = nullptr;
for (const auto& br : spec.badges) {
if (br.value == value) { matched = &br; break; }
}
if (matched) {
ImVec4 col = hex_to_imcolor(matched->color_hex);
const char* label = matched->label.empty() ? value : matched->label.c_str();
if (col.x >= 0.f) {
ImGui::PushStyleColor(ImGuiCol_Header, col);
// Slightly brighter on hover to provide feedback.
ImVec4 hover_col = ImVec4(
std::min(col.x + 0.1f, 1.f),
std::min(col.y + 0.1f, 1.f),
std::min(col.z + 0.1f, 1.f),
col.w);
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, hover_col);
ImGui::PushStyleColor(ImGuiCol_HeaderActive, hover_col);
// Issue 0081-O.7: removed SpanAllColumns — badge hover must not
// illuminate the entire row, only the badge cell.
ImGui::Selectable(label, false);
ImGui::PopStyleColor(3);
} else {
ImGui::TextUnformatted(label);
}
} else {
ImGui::TextUnformatted(value);
}
break;
}
case CellRenderer::Progress: {
double v_raw = 0.0;
if (!parse_number(value, v_raw)) {
ImGui::TextUnformatted(value);
break;
}
float fv = (float)v_raw;
if (spec.progress_scale_100) fv /= 100.f;
fv = fv < 0.f ? 0.f : (fv > 1.f ? 1.f : fv);
bool has_color = !spec.progress_color_hex.empty();
ImVec4 bar_col;
if (has_color) {
bar_col = hex_to_imcolor(spec.progress_color_hex);
if (bar_col.x < 0.f) has_color = false;
}
if (has_color) ImGui::PushStyleColor(ImGuiCol_PlotHistogram, bar_col);
ImGui::ProgressBar(fv, ImVec2(-1.f, 0.f));
if (has_color) ImGui::PopStyleColor();
break;
}
case CellRenderer::Duration: {
double v_raw = 0.0;
if (!parse_number(value, v_raw)) {
ImGui::TextUnformatted(value);
break;
}
float ms = (float)v_raw;
ImVec4 text_col;
if (ms <= spec.duration_warn_ms) {
text_col = hex_to_imcolor("#22c55e"); // green
} else if (ms <= spec.duration_error_ms) {
text_col = hex_to_imcolor("#f59e0b"); // yellow
} else {
text_col = hex_to_imcolor("#ef4444"); // red
}
char buf[32];
std::snprintf(buf, sizeof(buf), "%.0f ms", ms);
ImGui::TextColored(text_col, "%s", buf);
break;
}
case CellRenderer::Icon: {
const char* glyph = nullptr;
ImVec4 icon_col(-1.f, -1.f, -1.f, -1.f);
for (const auto& entry : spec.icon_map) {
if (entry.value == value) {
glyph = icon_name_to_glyph(entry.icon_name);
if (!entry.color_hex.empty())
icon_col = hex_to_imcolor(entry.color_hex);
break;
}
}
if (glyph) {
if (icon_col.x >= 0.f)
ImGui::TextColored(icon_col, "%s", glyph);
else
ImGui::TextUnformatted(glyph);
} else {
ImGui::TextUnformatted(value);
}
break;
}
case CellRenderer::Button: {
// Skip empty cell values — app decides when to show a button.
if (value[0] == '\0') break;
const char* label = spec.button_label.empty() ? value : spec.button_label.c_str();
bool has_color = !spec.button_color_hex.empty();
if (has_color) {
ImVec4 btn_col = hex_to_imcolor(spec.button_color_hex);
if (btn_col.x >= 0.f) {
ImGui::PushStyleColor(ImGuiCol_Button, btn_col);
ImVec4 hov = ImVec4(
std::min(btn_col.x + 0.12f, 1.f),
std::min(btn_col.y + 0.12f, 1.f),
std::min(btn_col.z + 0.12f, 1.f),
btn_col.w);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, hov);
ImGui::PushStyleColor(ImGuiCol_ButtonActive, hov);
} else {
has_color = false;
}
}
// Unique button ID: combines label + row + col to avoid ImGui ID
// collisions when the same label appears in multiple rows.
char btn_id[128];
std::snprintf(btn_id, sizeof(btn_id), "%s##btn_%d_%d",
label, row_idx, col_idx);
if (ImGui::SmallButton(btn_id) && events_out) {
TableEvent ev;
ev.kind = TableEventKind::ButtonClick;
ev.row = row_idx;
ev.col = col_idx;
ev.column_id = spec.id;
ev.action_id = spec.button_action;
ev.value = value;
events_out->push_back(std::move(ev));
}
if (has_color) ImGui::PopStyleColor(3);
break;
}
case CellRenderer::Dots: {
// Parse cell value as separator-delimited tokens.
std::string s = value;
std::vector<std::string> tokens;
{
std::string cur;
char sep = spec.dots_separator ? spec.dots_separator : ',';
for (char c : s) {
if (c == sep) { tokens.push_back(cur); cur.clear(); }
else cur.push_back(c);
}
if (!cur.empty()) tokens.push_back(cur);
}
int limit = (spec.dots_max > 0)
? std::min((int)tokens.size(), spec.dots_max)
: (int)tokens.size();
// Draw filled circles via ImDrawList — font-independent, scales with font size.
// BadgeRule.label is ignored for Dots (only relevant for Badge renderer).
float font_h = ImGui::GetTextLineHeight();
float radius = (spec.dots_glyph_size > 0.f ? spec.dots_glyph_size : font_h * 0.32f);
float spacing = radius * 2.3f;
ImVec2 origin = ImGui::GetCursorScreenPos();
float dot_y = origin.y + font_h * 0.5f;
ImDrawList* dl = ImGui::GetWindowDrawList();
ImU32 default_col = IM_COL32(110, 110, 125, 255); // muted grey
for (int t = 0; t < limit; ++t) {
ImU32 col = default_col;
for (const auto& br : spec.badges) {
if (br.value == tokens[t]) {
ImVec4 c = hex_to_imcolor(br.color_hex);
if (c.x >= 0.f) col = ImGui::ColorConvertFloat4ToU32(c);
break;
}
}
float cx = origin.x + radius + t * spacing;
dl->AddCircleFilled(ImVec2(cx, dot_y), radius, col, 18);
}
// Reserve cursor space so layout flows correctly.
float total_w = (limit > 0 ? (limit * spacing) : 0.f);
ImGui::Dummy(ImVec2(total_w, font_h));
// Hit-test for tooltip per dot.
if (spec.tooltip_on_hover && ImGui::IsItemHovered()) {
ImVec2 mp = ImGui::GetMousePos();
for (int t = 0; t < limit; ++t) {
float cx = origin.x + radius + t * spacing;
float dx = mp.x - cx, dy = mp.y - dot_y;
if (dx*dx + dy*dy <= radius * radius * 1.5f) {
ImGui::SetTooltip("%s", tokens[t].c_str());
break;
}
}
}
if (spec.dots_show_count) {
ImGui::SameLine(0, 6.0f);
ImGui::TextDisabled("(%d)", (int)tokens.size());
}
break;
}
case CellRenderer::CategoricalChip: {
// Draw a filled circle to the LEFT of the cell text.
// Color determined by matching value against chips rules.
// Always visible (not hover-only). If no rule matches, no dot.
// v1.4.0.
const ChipRule* matched_chip = nullptr;
for (const auto& cr : spec.chips) {
if (cr.match == value) { matched_chip = &cr; break; }
}
if (matched_chip) {
float font_h = ImGui::GetTextLineHeight();
ImVec2 cursor = ImGui::GetCursorScreenPos();
float radius = 4.0f;
float cy = cursor.y + font_h * 0.5f;
float cx = cursor.x + radius;
ImDrawList* dl = ImGui::GetWindowDrawList();
ImU32 chip_col = parse_hex_color(matched_chip->color, 1.0f);
dl->AddCircleFilled(ImVec2(cx, cy), radius, chip_col, 18);
// Advance cursor past the dot + 4px gap.
ImGui::Dummy(ImVec2(radius * 2.0f + 4.0f, font_h));
ImGui::SameLine(0, 0);
}
ImGui::TextUnformatted(value);
break;
}
case CellRenderer::ColorScale: {
// Paint cell background with an interpolated color from N-stop gradient.
// Numeric value mapped to t = (val - range_min) / (range_max - range_min).
// Clamped to [0,1]. Non-parseable values render as plain text.
// v1.4.0.
double v_raw = 0.0;
if (!parse_number(value, v_raw)) {
ImGui::TextUnformatted(value);
break;
}
double span = spec.range_max - spec.range_min;
float t = 0.f;
if (span > 1e-12) {
t = (float)((v_raw - spec.range_min) / span);
}
// Clamp.
t = t < 0.f ? 0.f : (t > 1.f ? 1.f : t);
// Paint background rect covering the full cell area.
ImU32 bg_col = lerp_color_along_stops(spec.range_stops, t, spec.range_alpha);
ImVec2 cell_min = ImGui::GetCursorScreenPos();
// Use cell size: full column width × row height.
float row_h = ImGui::GetTextLineHeight();
float col_w = ImGui::GetContentRegionAvail().x;
ImVec2 cell_max = ImVec2(cell_min.x + col_w, cell_min.y + row_h);
ImGui::GetWindowDrawList()->AddRectFilled(cell_min, cell_max, bg_col);
// Draw text on top.
ImGui::TextUnformatted(value);
break;
}
default:
// CellRenderer::Text or unknown — plain text.
ImGui::TextUnformatted(value);
break;
}
// Tooltip: show on hover if tooltip_on_hover is set (non-Dots renderers).
// For Dots, per-dot tooltips are handled inline above.
// "auto" shows the raw cell value (useful for truncated text columns).
if (spec.renderer != CellRenderer::Dots &&
spec.tooltip_on_hover && ImGui::IsItemHovered()) {
const char* tip = (spec.tooltip == "auto") ? value : spec.tooltip.c_str();
if (tip && tip[0]) ImGui::SetTooltip("%s", tip);
}
}
// compare: cell-level comparison supporting all Op variants.
// Uses parse_number (from auto_detect_type.h) for numeric comparisons.
static bool compare(const char* a, const char* b, Op op) {
if (!a) a = "";
if (!b) b = "";
switch (op) {
case Op::Contains: return std::strstr(a, b) != nullptr;
case Op::NotContains: return std::strstr(a, b) == nullptr;
case Op::StartsWith: {
size_t lb = std::strlen(b);
return std::strncmp(a, b, lb) == 0;
}
case Op::EndsWith: {
size_t la = std::strlen(a), lb = std::strlen(b);
return lb <= la && std::strcmp(a + la - lb, b) == 0;
}
default: break;
}
double na, nb;
bool numeric = parse_number(a, na) && parse_number(b, nb);
if (numeric) {
switch (op) {
case Op::Eq: return na == nb;
case Op::Neq: return na != nb;
case Op::Gt: return na > nb;
case Op::Gte: return na >= nb;
case Op::Lt: return na < nb;
case Op::Lte: return na <= nb;
default: break;
}
}
int c = std::strcmp(a, b);
switch (op) {
case Op::Eq: return c == 0;
case Op::Neq: return c != 0;
case Op::Gt: return c > 0;
case Op::Gte: return c >= 0;
case Op::Lt: return c < 0;
case Op::Lte: return c <= 0;
default: break;
}
return false;
}
// make_drill_filter: creates an Op::Eq filter on col_idx with the given value.
static Filter make_drill_filter(int col_idx, const std::string& value) {
Filter f;
f.col = col_idx;
f.op = Op::Eq;
f.value = value;
return f;
}
// apply_drill_step: inserts step.added into st at the recorded position.
static bool apply_drill_step(State& st, const DrillStep& step) {
if (step.target_stage < 0 || step.target_stage >= (int)st.stages.size()) return false;
Stage& s = st.stages[step.target_stage];
int pos = step.filter_pos;
if (pos < 0 || pos > (int)s.filters.size()) return false;
s.filters.insert(s.filters.begin() + pos, step.added);
st.active_stage = step.target_stage;
return true;
}
// undo_drill_step: removes the filter inserted by apply_drill_step.
static bool undo_drill_step(State& st, const DrillStep& step) {
if (step.target_stage < 0 || step.target_stage >= (int)st.stages.size()) return false;
Stage& s = st.stages[step.target_stage];
int pos = step.filter_pos;
if (pos < 0 || pos >= (int)s.filters.size()) return false;
s.filters.erase(s.filters.begin() + pos);
if (step.prev_active_stage >= 0 && step.prev_active_stage < (int)st.stages.size())
st.active_stage = step.prev_active_stage;
return true;
}
// drill_up: decrements active_stage by 1 if possible.
static bool drill_up(State& st) {
if (st.stages.empty()) return false;
if (st.active_stage <= 0) return false;
st.active_stage -= 1;
return true;
}
// row_to_tsv: serializes a single row to a two-line TSV (header + values).
static std::string row_to_tsv(const char* const* cells, int rows, int cols,
int row_idx, const std::vector<std::string>& headers) {
if (row_idx < 0 || row_idx >= rows || cols <= 0) return "";
std::string out;
for (int c = 0; c < cols; ++c) {
if (c > 0) out += '\t';
if (c < (int)headers.size()) out += headers[c];
}
out += "\r\n";
for (int c = 0; c < cols; ++c) {
if (c > 0) out += '\t';
const char* v = cells[row_idx * cols + c];
if (v) out += v;
}
out += "\r\n";
return out;
}
// compute_visible_rows: applies stage-0 filters + optional sort, returns matching row indices.
static std::vector<int> compute_visible_rows(const char* const* cells,
int rows, int cols,
const State& st)
{
std::vector<int> out;
out.reserve(rows);
const Stage& s = st.raw();
for (int r = 0; r < rows; ++r) {
bool keep = true;
for (const auto& f : s.filters) {
if (f.col < 0 || f.col >= cols) continue;
const char* cell = cells[r * cols + f.col];
if (!compare(cell, f.value.c_str(), f.op)) { keep = false; break; }
}
if (keep) out.push_back(r);
}
if (!s.sorts.empty()) {
const SortClause& sc0 = s.sorts.front();
int sc = -1;
if (!sc0.col.empty() && sc0.col[0] == '@') {
sc = std::atoi(sc0.col.c_str() + 1);
}
bool desc = sc0.desc;
if (sc >= 0 && sc < cols) {
std::sort(out.begin(), out.end(), [&](int a, int b) {
const char* ca = cells[a * cols + sc];
const char* cb = cells[b * cols + sc];
if (!ca) ca = "";
if (!cb) cb = "";
double na, nb;
bool num = parse_number(ca, na) && parse_number(cb, nb);
int cmp;
if (num) cmp = (na < nb) ? -1 : (na > nb ? 1 : 0);
else cmp = std::strcmp(ca, cb);
return desc ? (cmp > 0) : (cmp < 0);
});
}
}
return out;
}
// csv_escape: wraps s in double-quotes if it contains commas, quotes, or newlines.
static std::string csv_escape(const char* s) {
if (!s) return "";
bool needs = false;
for (const char* p = s; *p; ++p) {
if (*p == ',' || *p == '"' || *p == '\n' || *p == '\r') { needs = true; break; }
}
if (!needs) return std::string(s);
std::string out; out.reserve(std::strlen(s) + 4);
out += '"';
for (const char* p = s; *p; ++p) {
if (*p == '"') out += '"';
out += *p;
}
out += '"';
return out;
}
// reorder_column: moves col src to position of col dst in st.col_order.
static void reorder_column(State& st, int src, int dst) {
if (src == dst) return;
auto it_s = std::find(st.col_order.begin(), st.col_order.end(), src);
auto it_d = std::find(st.col_order.begin(), st.col_order.end(), dst);
if (it_s == st.col_order.end() || it_d == st.col_order.end()) return;
int si = (int)(it_s - st.col_order.begin());
int di = (int)(it_d - st.col_order.begin());
int v = st.col_order[si];
st.col_order.erase(st.col_order.begin() + si);
if (di > (int)st.col_order.size()) di = (int)st.col_order.size();
st.col_order.insert(st.col_order.begin() + di, v);
}
// find_open_bracket: scans buf[0..cursor) backwards for an unmatched '['.
// Returns index of '[' and fills filter_text with content after it, or -1 if none.
static int find_open_bracket(const char* buf, int len, int cursor, std::string& filter_text) {
filter_text.clear();
if (!buf || cursor <= 0 || cursor > len) return -1;
for (int i = cursor - 1; i >= 0; --i) {
char c = buf[i];
if (c == ']' || c == '\n') return -1;
if (c == '[') {
filter_text.assign(buf + i + 1, cursor - i - 1);
return i;
}
}
return -1;
}
// insert_column_ref: replaces src[start..cursor) with "[name]", updating new_cursor.
static std::string insert_column_ref(const std::string& src, int start, int cursor,
const std::string& name, int& new_cursor) {
if (start < 0 || start > (int)src.size() || cursor < start || cursor > (int)src.size()) {
new_cursor = cursor;
return src;
}
std::string replacement = "[" + name + "]";
std::string out;
out.reserve(src.size() - (cursor - start) + replacement.size());
out.append(src, 0, start);
out += replacement;
out.append(src, cursor, std::string::npos);
new_cursor = start + (int)replacement.size();
return out;
}
// ---------------------------------------------------------------------------
// Additional helpers from playground data_table_logic — view_mode, joins,
// filter presets, date helpers, effective_type, etc.
// All declared static to stay internal to this translation unit.
// ---------------------------------------------------------------------------
static ColumnType effective_type(ColumnType declared,
const char* const* cells, int rows, int cols, int col) {
if (declared != ColumnType::Auto) return declared;
return auto_detect_type(cells, rows, cols, col);
}
static std::vector<Op> ops_for_type(ColumnType t) {
switch (t) {
case ColumnType::Int:
case ColumnType::Float:
case ColumnType::Date:
return {Op::Eq, Op::Neq, Op::Gt, Op::Gte, Op::Lt, Op::Lte};
case ColumnType::Bool:
return {Op::Eq, Op::Neq};
case ColumnType::Json:
return {Op::Eq, Op::Neq, Op::Contains, Op::NotContains};
case ColumnType::String:
return {Op::Eq, Op::Neq, Op::Contains, Op::NotContains, Op::StartsWith, Op::EndsWith};
case ColumnType::Auto:
default:
return {Op::Eq, Op::Neq, Op::Contains, Op::NotContains};
}
}
static int resolve_main_idx(const std::vector<TableInput>& tables, const std::string& main_source) {
if (tables.empty()) return -1;
if (main_source.empty()) return 0;
for (size_t i = 0; i < tables.size(); ++i) {
if (tables[i].name == main_source) return (int)i;
}
return 0;
}
static const char* join_strategy_label(JoinStrategy s) {
switch (s) {
case JoinStrategy::Left: return "left-join";
case JoinStrategy::Inner: return "inner-join";
case JoinStrategy::Right: return "right-join";
case JoinStrategy::Full: return "full-join";
}
return "left-join";
}
struct ViewModeInfo {
ViewMode m;
const char* token;
const char* label;
int min_cols;
bool needs_num;
bool needs_cat;
bool needs_agg;
};
static const ViewModeInfo kViewModes[] = {
{ ViewMode::Table, "table", "Table", 1, false, false, false },
{ ViewMode::Bar, "bar", "Bar (horizontal)", 2, true, true, true },
{ ViewMode::Column, "column", "Column (vertical)", 2, true, true, true },
{ ViewMode::GroupedBar, "grouped_bar", "Grouped bar", 2, true, true, true },
{ ViewMode::StackedBar, "stacked_bar", "Stacked bar", 2, true, true, true },
{ ViewMode::Line, "line", "Line", 1, true, false, false },
{ ViewMode::Area, "area", "Area", 1, true, false, false },
{ ViewMode::Stairs, "stairs", "Stairs", 1, true, false, false },
{ ViewMode::Scatter, "scatter", "Scatter", 2, true, false, false },
{ ViewMode::Bubble, "bubble", "Bubble", 3, true, false, false },
{ ViewMode::Histogram, "histogram", "Histogram", 1, true, false, false },
{ ViewMode::Histogram2D, "hist2d", "Histogram 2D", 2, true, false, false },
{ ViewMode::Heatmap, "heatmap", "Heatmap", 1, true, false, false },
{ ViewMode::BoxPlot, "boxplot", "Box plot", 2, true, true, false },
{ ViewMode::Stem, "stem", "Stem", 1, true, false, false },
{ ViewMode::ErrorBars, "errorbars", "Error bars", 2, true, false, false },
{ ViewMode::Pie, "pie", "Pie", 2, true, true, true },
{ ViewMode::Donut, "donut", "Donut", 2, true, true, true },
{ ViewMode::Funnel, "funnel", "Funnel", 2, true, true, true },
{ ViewMode::Waterfall, "waterfall", "Waterfall", 1, true, false, true },
{ ViewMode::KPI, "kpi", "KPI (single)", 1, true, false, true },
{ ViewMode::KPIGrid, "kpi_grid", "KPI grid", 1, true, false, true },
{ ViewMode::Candlestick, "candlestick", "Candlestick (OHLC)", 4, true, false, false },
{ ViewMode::Radar, "radar", "Radar", 2, true, true, false },
};
static const int kViewModesN = (int)(sizeof(kViewModes) / sizeof(kViewModes[0]));
static const char* view_mode_label(ViewMode m) {
for (int i = 0; i < kViewModesN; ++i) if (kViewModes[i].m == m) return kViewModes[i].label;
return "Table";
}
static bool view_mode_needs_aggregation(ViewMode m) {
for (int i = 0; i < kViewModesN; ++i) if (kViewModes[i].m == m) return kViewModes[i].needs_agg;
return false;
}
static const ViewMode* all_view_modes(int* n_out) {
static ViewMode arr[64];
static bool init = false;
if (!init) {
for (int i = 0; i < kViewModesN; ++i) arr[i] = kViewModes[i].m;
init = true;
}
if (n_out) *n_out = kViewModesN;
return arr;
}
// Date helpers (for filter presets and breakout auto-granularity).
namespace {
static bool parse_ymd_local(const std::string& s, int& y, int& m, int& d) {
if (s.size() < 10) return false;
for (int i : {0,1,2,3,5,6,8,9}) {
if (s[(size_t)i] < '0' || s[(size_t)i] > '9') return false;
}
if (s[4] != '-' || s[7] != '-') return false;
y = (s[0]-'0')*1000 + (s[1]-'0')*100 + (s[2]-'0')*10 + (s[3]-'0');
m = (s[5]-'0')*10 + (s[6]-'0');
d = (s[8]-'0')*10 + (s[9]-'0');
if (m < 1 || m > 12 || d < 1 || d > 31) return false;
return true;
}
static long ymd_to_days_local(int y, int m, int d) {
if (m <= 2) { y -= 1; m += 12; }
long era = (y >= 0 ? y : y - 399) / 400;
unsigned yoe = (unsigned)(y - era * 400);
unsigned doy = (unsigned)((153 * (m - 3) + 2) / 5 + d - 1);
unsigned doe = yoe * 365 + yoe/4 - yoe/100 + doy;
return era * 146097 + (long)doe;
}
static void days_to_ymd_local(long days, int& y, int& m, int& d) {
long era = (days >= 0 ? days : days - 146096) / 146097;
unsigned doe = (unsigned)(days - era * 146097);
unsigned yoe = (doe - doe/1460 + doe/36524 - doe/146096) / 365;
int yr = (int)yoe + (int)era * 400;
unsigned doy = doe - (365*yoe + yoe/4 - yoe/100);
unsigned mp = (5*doy + 2)/153;
unsigned day = doy - (153*mp + 2)/5 + 1;
unsigned mon = mp < 10 ? mp + 3 : mp - 9;
if (mon <= 2) yr += 1;
y = yr; m = (int)mon; d = (int)day;
}
} // anon
static void column_min_max(const char* const* cells, int rows, int cols, int col_idx,
std::string& min_out, std::string& max_out) {
min_out.clear(); max_out.clear();
if (col_idx < 0 || col_idx >= cols) return;
bool first = true;
for (int r = 0; r < rows; ++r) {
const char* v = cells[r * cols + col_idx];
if (!v || !*v) continue;
std::string s(v);
if (first) { min_out = s; max_out = s; first = false; }
else { if (s < min_out) min_out = s; if (s > max_out) max_out = s; }
}
}
static DateGranularity auto_date_granularity(const std::string& min_ymd,
const std::string& max_ymd) {
int y1,m1,d1, y2,m2,d2;
if (!parse_ymd_local(min_ymd, y1,m1,d1)) return DateGranularity::Day;
if (!parse_ymd_local(max_ymd, y2,m2,d2)) return DateGranularity::Day;
long span = ymd_to_days_local(y2,m2,d2) - ymd_to_days_local(y1,m1,d1);
if (span < 0) span = -span;
if (span > 730) return DateGranularity::Year;
if (span > 60) return DateGranularity::Month;
if (span > 14) return DateGranularity::Week;
return DateGranularity::Day;
}
static std::string compose_breakout(const std::string& col, DateGranularity g) {
if (g == DateGranularity::None) return col;
return col + ":" + date_granularity_token(g);
}
static const char* filter_preset_label(FilterPreset p) {
switch (p) {
case FilterPreset::Last7d: return "Last 7 days";
case FilterPreset::Last30d: return "Last 30 days";
case FilterPreset::Last90d: return "Last 90 days";
case FilterPreset::ExcludeNulls: return "Exclude nulls";
case FilterPreset::NonZero: return "Non-zero only";
}
return "?";
}
static std::vector<Filter> build_preset_filters(FilterPreset preset, int col,
const std::string& today_ymd) {
std::vector<Filter> out;
auto last_n = [&](int n) {
int y, m, d;
if (!parse_ymd_local(today_ymd, y, m, d)) return;
long days = ymd_to_days_local(y, m, d) - n;
int yy, mm, dd;
days_to_ymd_local(days, yy, mm, dd);
char buf[16];
std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d", yy, mm, dd);
Filter f; f.col = col; f.op = Op::Gte; f.value = buf;
out.push_back(f);
};
switch (preset) {
case FilterPreset::Last7d: last_n(7); break;
case FilterPreset::Last30d: last_n(30); break;
case FilterPreset::Last90d: last_n(90); break;
case FilterPreset::ExcludeNulls: {
Filter f; f.col = col; f.op = Op::Neq; f.value = "";
out.push_back(f); break;
}
case FilterPreset::NonZero: {
Filter f1; f1.col = col; f1.op = Op::Neq; f1.value = "";
Filter f2; f2.col = col; f2.op = Op::Neq; f2.value = "0";
out.push_back(f1); out.push_back(f2); break;
}
}
return out;
}
// agg_fn_name, op_is_string_only — small helpers not in tql_helpers.h.
// op_label and aggregation_alias are already provided by tql_helpers.h.
static const char* agg_fn_name(AggFn f) {
switch (f) {
case AggFn::Count: return "count";
case AggFn::Sum: return "sum";
case AggFn::Avg: return "avg";
case AggFn::Min: return "min";
case AggFn::Max: return "max";
case AggFn::Distinct: return "distinct";
case AggFn::Stddev: return "stddev";
case AggFn::Median: return "median";
case AggFn::P25: return "p25";
case AggFn::P75: return "p75";
case AggFn::P90: return "p90";
case AggFn::P99: return "p99";
case AggFn::Percentile: return "percentile";
}
return "?";
}
static bool op_is_string_only(Op o) {
return o == Op::Contains || o == Op::NotContains ||
o == Op::StartsWith || o == Op::EndsWith;
}
// UTC date today as ISO YYYY-MM-DD. Para preset filtros Last7/30/90d.
static std::string today_iso() {
std::time_t t = std::time(nullptr);
std::tm tm = *std::gmtime(&t);
char buf[16];
std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday);
return buf;
}
namespace {
// ---------------------------------------------------------------------------
// UI state global por-instancia (singleton playground).
// ---------------------------------------------------------------------------
struct UiState {
int pending_col = -1;
std::string pending_value;
bool open_cell_popup = false;
int header_popup_col = -1;
std::unordered_map<int, std::string> filter_inputs;
std::unordered_map<int, std::string> color_value_inputs;
std::unordered_map<int, ImVec4> color_picker_vals;
int addf_col = 0;
std::string addf_val;
bool addf_range = false;
std::string addf_lo;
std::string addf_hi;
int sel_anchor_row = -1;
int sel_anchor_col = -1;
int sel_end_row = -1;
int sel_end_col = -1;
bool sel_active = false;
bool sel_dragging = false;
std::string last_export_path;
// Modal de columna custom (formula).
bool cf_open = false;
bool cf_editing = false;
int cf_edit_idx = -1;
int cf_target_stage = 0; // stage donde se guarda la formula
std::string cf_formula;
std::string cf_name;
ColumnType cf_type = ColumnType::String;
std::string cf_error;
bool cf_ac_open = false;
int cf_ac_start = -1;
int cf_ac_cursor = -1;
std::string cf_ac_filter;
bool cf_force_cursor = false;
int cf_target_cursor = -1;
// TQL modales.
bool tql_show_open = false;
std::string tql_show_text;
bool tql_apply_open = false;
std::string tql_apply_text;
std::string tql_apply_error;
char tql_file_path[256] = "table.tql";
std::string tql_io_status; // mensaje "saved" / "loaded" / error
// Add-breakout popup (stage > 0).
int brk_picker_col = 0;
// Add-aggregation popup (stage > 0).
int agg_picker_fn = (int)AggFn::Count;
int agg_picker_col = 0;
double agg_picker_arg = 0.95;
// Edit chip popups: click der sobre chip.
// 0=none, 1=filter, 2=breakout, 3=agg, 4=sort
int edit_chip_kind = 0;
int edit_chip_idx = -1;
int edit_col_idx = 0; // combo idx para col picker
int edit_op = (int)Op::Eq;
int edit_agg_fn = (int)AggFn::Count;
double edit_agg_arg = 0.5;
bool edit_sort_desc = false;
std::string edit_value;
// Add-sort popup (any stage).
int sort_picker_col = 0;
bool sort_picker_desc = false;
bool stats_mode = false;
std::vector<ColStats> stats_cache;
const char* const* last_cells = nullptr;
int last_rows = -1;
int last_eff_cols = -1;
size_t last_filter_h = (size_t)-1;
int last_visible = -1;
// Snapshot del active stage output para el config popup.
std::vector<std::string> active_headers;
std::vector<ColumnType> active_types;
// Snapshot del INPUT del active stage (= output del previo o orig+derived
// si active==0). Para que el config popup pueda cambiar la categoria del
// breakout del stage activo eligiendo de las cols upstream.
std::vector<std::string> input_headers_active;
std::vector<ColumnType> input_types_active;
// Para forzar re-fit en cambio de display/stage/config.
ViewMode prev_viz_display = ViewMode::Table;
int prev_viz_stage = 0;
size_t prev_viz_cfg_h = 0;
// (chrome_user_set / chrome_user_visible moved to State — per-table now.)
// Toggle Table <-> View: remember last non-table display.
ViewMode last_non_table_main = ViewMode::Bar;
// Drill history (fase 10). Stacks per-app; no persistido en TQL.
std::vector<DrillStep> drill_back;
std::vector<DrillStep> drill_forward;
// Row inspector (fase 10). -1 cerrado, sino row idx en el output del stage activo.
int inspect_row = -1;
bool inspect_open = false;
// Ask AI modal (fase 11 — issue 0080).
bool ask_open = false;
bool ask_busy = false;
int ask_mode = 0; // 0 = TQL, 1 = SQL
char ask_question[2048] = {0};
std::string ask_current_tql; // emit del state actual al abrir modal
std::string ask_response_raw; // texto del modelo
std::string ask_response_code; // bloque extraido (Lua o SQL)
std::string ask_error;
std::string ask_status; // "Sent. Waiting..." / "OK" / error
char ask_edit_buf[8192] = {0}; // buffer editable de propuesta
};
UiState& ui() { static UiState s; return s; }
// Row inspector modal (fase 10). Muestra todas cols + valores de la fila
// inspect_row del output del stage activo. Read-only + Copy TSV + Filter
// by this row (anade filters al stage previo si existe).
static void draw_row_inspector_modal(State& st, int active,
const char* const* cells, int rows, int cols,
const std::vector<std::string>& headers,
const std::vector<ColumnType>& types,
const std::vector<std::string>& prev_input_headers) {
auto& U = ui();
if (!U.inspect_open) return;
if (U.inspect_row < 0 || U.inspect_row >= rows) {
U.inspect_open = false;
return;
}
ImGui::OpenPopup("##row_inspector");
ImGui::SetNextWindowSize(ImVec2(560, 400), ImGuiCond_Appearing);
if (ImGui::BeginPopupModal("##row_inspector", &U.inspect_open,
ImGuiWindowFlags_NoSavedSettings)) {
ImGui::Text("Row %d", U.inspect_row);
ImGui::SameLine(0, 20);
if (ImGui::SmallButton("Copy TSV")) {
std::string tsv = row_to_tsv(cells, rows, cols, U.inspect_row, headers);
ImGui::SetClipboardText(tsv.c_str());
}
ImGui::SameLine();
bool can_filter = (active > 0 && !prev_input_headers.empty());
ImGui::BeginDisabled(!can_filter);
if (ImGui::SmallButton("Filter prev stage by this row")) {
int target = active - 1;
for (int c = 0; c < cols; ++c) {
const char* v = cells[U.inspect_row * cols + c];
if (!v || !*v) continue;
const std::string& h = headers[c];
std::string h_clean;
parse_breakout_granularity(h, h_clean);
int ci = -1;
for (size_t i = 0; i < prev_input_headers.size(); ++i) {
if (prev_input_headers[i] == h_clean) { ci = (int)i; break; }
}
if (ci < 0) continue;
DrillStep step;
step.target_stage = target;
step.filter_pos = (int)st.stages[target].filters.size();
step.prev_active_stage = st.active_stage;
step.added = make_drill_filter(ci, v);
if (apply_drill_step(st, step)) {
U.drill_back.push_back(step);
}
}
U.drill_forward.clear();
U.inspect_open = false;
}
ImGui::EndDisabled();
ImGui::Separator();
ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg
| ImGuiTableFlags_ScrollY | ImGuiTableFlags_Resizable;
if (ImGui::BeginTable("##inspector_tbl", 2, flags, ImVec2(-1, -1))) {
ImGui::TableSetupColumn("col");
ImGui::TableSetupColumn("value");
ImGui::TableHeadersRow();
for (int c = 0; c < cols; ++c) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ColumnType t = (c < (int)types.size()) ? types[c] : ColumnType::String;
ImGui::Text("%s %s", column_type_icon(t),
(c < (int)headers.size()) ? headers[c].c_str() : "?");
ImGui::TableSetColumnIndex(1);
const char* v = cells[U.inspect_row * cols + c];
ImGui::TextWrapped("%s", v ? v : "");
}
ImGui::EndTable();
}
ImGui::EndPopup();
}
}
int autocomplete_cb(ImGuiInputTextCallbackData* data) {
UiState* U = (UiState*)data->UserData;
if (data->EventFlag == ImGuiInputTextFlags_CallbackAlways) {
if (U->cf_force_cursor) {
data->CursorPos = U->cf_target_cursor;
U->cf_force_cursor = false;
}
}
if (data->EventFlag == ImGuiInputTextFlags_CallbackEdit) {
std::string filter;
int idx = find_open_bracket(data->Buf, data->BufTextLen,
data->CursorPos, filter);
if (idx >= 0) {
U->cf_ac_open = true;
U->cf_ac_start = idx;
U->cf_ac_cursor = data->CursorPos;
U->cf_ac_filter = filter;
} else {
U->cf_ac_open = false;
}
}
return 0;
}
size_t filters_hash(const std::vector<Filter>& f) {
size_t h = 0xcbf29ce484222325ULL;
for (const auto& x : f) {
h ^= (size_t)x.col; h *= 0x100000001b3ULL;
h ^= (size_t)x.op; h *= 0x100000001b3ULL;
for (char ch : x.value) { h ^= (size_t)(unsigned char)ch; h *= 0x100000001b3ULL; }
}
return h;
}
void ensure_init(State& st, int eff_cols) {
if ((int)st.col_visible.size() < eff_cols) st.col_visible.resize(eff_cols, true);
if ((int)st.col_order.size() != eff_cols) {
std::vector<int> next;
next.reserve(eff_cols);
for (int x : st.col_order) if (x >= 0 && x < eff_cols) next.push_back(x);
std::vector<bool> seen(eff_cols, false);
for (int x : next) seen[x] = true;
for (int i = 0; i < eff_cols; ++i) if (!seen[i]) next.push_back(i);
st.col_order = std::move(next);
}
if ((int)st.col_visible.size() < (int)st.col_order.size())
st.col_visible.resize(st.col_order.size(), true);
}
// ---------------------------------------------------------------------------
// Breadcrumb stages: fila de botones Raw > Stage 1 > Stage 2 ... [+ Stage]
// ---------------------------------------------------------------------------
void draw_stage_breadcrumb(State& st) {
st.ensure_stage0();
// Drill history back/forward (fase 10). Botones al inicio.
auto& U = ui();
{
bool can_back = !U.drill_back.empty();
ImGui::BeginDisabled(!can_back);
if (ImGui::SmallButton("<##drill_back")) {
DrillStep s = U.drill_back.back();
U.drill_back.pop_back();
if (undo_drill_step(st, s)) {
U.drill_forward.push_back(s);
}
}
ImGui::EndDisabled();
if (can_back && ImGui::IsItemHovered())
ImGui::SetTooltip("Drill back (%zu)", U.drill_back.size());
ImGui::SameLine();
bool can_fwd = !U.drill_forward.empty();
ImGui::BeginDisabled(!can_fwd);
if (ImGui::SmallButton(">##drill_fwd")) {
DrillStep s = U.drill_forward.back();
U.drill_forward.pop_back();
if (apply_drill_step(st, s)) {
U.drill_back.push_back(s);
}
}
ImGui::EndDisabled();
if (can_fwd && ImGui::IsItemHovered())
ImGui::SetTooltip("Drill forward (%zu)", U.drill_forward.size());
ImGui::SameLine();
bool can_up = (st.active_stage > 0);
ImGui::BeginDisabled(!can_up);
if (ImGui::SmallButton("^##drill_up")) drill_up(st);
ImGui::EndDisabled();
if (can_up && ImGui::IsItemHovered())
ImGui::SetTooltip("Drill up (stage previo, sin perder filters)");
ImGui::SameLine();
ImGui::TextDisabled("|");
ImGui::SameLine();
}
for (int si = 0; si < (int)st.stages.size(); ++si) {
if (si > 0) { ImGui::SameLine(); ImGui::TextDisabled(">"); ImGui::SameLine(); }
bool active = (si == st.active_stage);
if (active) {
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32( 80, 140, 200, 240));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(100, 160, 220, 240));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 60, 120, 180, 240));
} else {
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32( 70, 70, 90, 200));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32( 90, 90, 120, 220));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 55, 55, 75, 220));
}
char label[256];
if (si == 0) {
std::snprintf(label, sizeof(label), "Raw##stage%d", si);
} else {
const Stage& s = st.stages[si];
std::string desc;
for (size_t i = 0; i < s.breakouts.size() && i < 2; ++i) {
if (i > 0) desc += ", ";
desc += s.breakouts[i];
}
if (s.breakouts.size() > 2) desc += "...";
if (desc.empty())
std::snprintf(label, sizeof(label), "Stage %d##s%d", si, si);
else
std::snprintf(label, sizeof(label), "Stage %d: by %s##s%d",
si, desc.c_str(), si);
}
if (ImGui::Button(label)) st.active_stage = si;
ImGui::PopStyleColor(3);
if (si > 0) {
ImGui::SameLine();
char xlbl[32];
std::snprintf(xlbl, sizeof(xlbl), "x##rm_s%d", si);
if (ImGui::SmallButton(xlbl)) {
// borra ese stage y sucesores
while ((int)st.stages.size() > si) st.stages.pop_back();
if (st.active_stage >= (int)st.stages.size())
st.active_stage = (int)st.stages.size() - 1;
if (st.active_stage < 0) st.active_stage = 0;
break;
}
}
}
ImGui::SameLine();
ImGui::TextDisabled(">");
ImGui::SameLine();
if (ImGui::SmallButton("+ Stage##add_stage")) {
st.stages.push_back(Stage{});
st.active_stage = (int)st.stages.size() - 1;
}
}
struct ColInfo { std::string name; ColumnType type; };
std::vector<ColInfo> collect_active_col_info(const State& st);
// Auto-promote: si user en stage 0 elige una viz que necesita agrupacion,
// crea stage 1 con breakout=primera cat + agg=sum(primera num) o count.
void auto_promote_aggregated(State& st) {
auto& U = ui();
if (st.active_stage != 0) return;
if (st.stages.size() != 1) return;
std::string cat_name;
std::string num_name;
for (size_t i = 0; i < U.active_headers.size() && i < U.active_types.size(); ++i) {
ColumnType t = U.active_types[i];
if (cat_name.empty() &&
(t == ColumnType::String || t == ColumnType::Date ||
t == ColumnType::Bool || t == ColumnType::Json)) {
cat_name = U.active_headers[i];
}
if (num_name.empty() &&
(t == ColumnType::Int || t == ColumnType::Float)) {
num_name = U.active_headers[i];
}
}
Stage s1;
if (!cat_name.empty()) s1.breakouts.push_back(cat_name);
Aggregation a;
if (!num_name.empty()) {
a.fn = AggFn::Sum;
a.col = num_name;
} else {
a.fn = AggFn::Count;
}
s1.aggregations.push_back(a);
st.stages.push_back(std::move(s1));
st.active_stage = (int)st.stages.size() - 1;
}
// Toggle simple: un solo boton que alterna entre Table y el last_non_table.
// Para el main pasa st (para poder auto-promote a stage agregado si la viz
// destino lo requiere). Para extras usar overload sin State.
void draw_table_toggle(ViewMode& display, ViewMode& last_non_table,
const char* id_suffix, State* st_opt = nullptr) {
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);
}
// Render extra viz panel: child window con toolbar mini + chart.
// Devuelve true si user pidio cerrar.
bool draw_extra_panel(State& st, VizPanel& p, int idx, const StageOutput& so,
const std::vector<ColumnSpec>* col_specs = nullptr) {
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;
}
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();
// Show checkbox for each numeric col
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 r = (unsigned int)(col_f[0] * 255);
unsigned int g = (unsigned int)(col_f[1] * 255);
unsigned int b = (unsigned int)(col_f[2] * 255);
unsigned int a = (unsigned int)(col_f[3] * 255);
vc.primary_color = (a << 24) | (b << 16) | (g << 8) | r;
}
ImGui::SameLine();
if (ImGui::SmallButton("Auto##color")) vc.primary_color = 0;
// Hist bins
if (m == ViewMode::Histogram || m == ViewMode::Histogram2D) {
ImGui::SetNextItemWidth(120);
int b = vc.hist_bins;
if (ImGui::InputInt("Bins (0=auto)", &b)) {
if (b < 0) b = 0;
vc.hist_bins = b;
}
}
// Pie radius
if (m == ViewMode::Pie || m == ViewMode::Donut) {
ImGui::SetNextItemWidth(120);
float r = vc.pie_radius;
if (ImGui::SliderFloat("Radius (0=auto)", &r, 0.0f, 0.5f, "%.2f")) {
vc.pie_radius = r;
}
}
// 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();
}
// Devuelve nombres + tipos del active stage output snapshot (poblado por render).
std::vector<ColInfo> collect_active_col_info(const State& st) {
(void)st;
auto& U = ui();
std::vector<ColInfo> r;
int n = (int)std::min(U.active_headers.size(), U.active_types.size());
r.reserve(n);
for (int i = 0; i < n; ++i) r.push_back({U.active_headers[i], U.active_types[i]});
return r;
}
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] [+ 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_open = true;
U2.ask_busy = false;
U2.ask_error.clear();
U2.ask_status.clear();
U2.ask_response_code.clear();
U2.ask_response_raw.clear();
U2.ask_current_tql = tql::emit(st,
std::vector<std::string>(), // emit headers stage 0 (caller fill si necesario)
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();
}
// ---------------------------------------------------------------------------
// Join chips (fase 9 — solo visible si hay joinables).
// ---------------------------------------------------------------------------
void draw_joins_chips(State& st, const std::vector<TableInput>& joinables,
const std::vector<std::string>& main_headers) {
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32( 80, 130, 90, 220));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(100, 160, 110, 240));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 60, 110, 70, 220));
ImGui::TextDisabled("Joins:");
ImGui::SameLine();
int remove_idx = -1;
for (size_t i = 0; i < st.joins.size(); ++i) {
const auto& jn = st.joins[i];
char lbl[256];
std::string on_str;
for (size_t k = 0; k < jn.on.size(); ++k) {
if (k) on_str += ",";
on_str += jn.on[k].first + "=" + jn.on[k].second;
}
std::snprintf(lbl, sizeof(lbl), "%s <- %s on %s (%s)##join_%zu",
jn.alias.empty() ? "_" : jn.alias.c_str(),
jn.source.c_str(),
on_str.c_str(),
join_strategy_label(jn.strategy),
i);
ImGui::Button(lbl);
ImGui::SameLine();
char xlbl[32]; std::snprintf(xlbl, sizeof(xlbl), "x##rm_join_%zu", i);
if (ImGui::SmallButton(xlbl)) remove_idx = (int)i;
ImGui::SameLine();
}
if (remove_idx >= 0) st.joins.erase(st.joins.begin() + remove_idx);
if (ImGui::SmallButton("+##add_join")) {
ImGui::OpenPopup("##add_join_popup");
}
ImGui::PopStyleColor(3);
// Popup add
static int pick_source_idx = 0;
static char pick_alias[64] = "";
static int pick_strategy = 0;
static int pick_left_col = 0;
static int pick_right_col = 0;
if (ImGui::BeginPopup("##add_join_popup")) {
ImGui::Text("Add join");
ImGui::SetNextItemWidth(180);
if (ImGui::BeginCombo("source", joinables[pick_source_idx].name.c_str())) {
for (int k = 0; k < (int)joinables.size(); ++k) {
bool sel = (k == pick_source_idx);
if (ImGui::Selectable(joinables[k].name.c_str(), sel)) {
pick_source_idx = k;
pick_right_col = 0;
if (pick_alias[0] == 0)
std::snprintf(pick_alias, sizeof(pick_alias), "%s", joinables[k].name.c_str());
}
}
ImGui::EndCombo();
}
ImGui::SetNextItemWidth(180);
ImGui::InputText("alias", pick_alias, sizeof(pick_alias));
const char* strategies[] = {"left", "inner", "right", "full"};
ImGui::SetNextItemWidth(120);
ImGui::Combo("strategy", &pick_strategy, strategies, IM_ARRAYSIZE(strategies));
// left col combo (de main_headers)
ImGui::SetNextItemWidth(180);
const char* lcur = (pick_left_col >= 0 && pick_left_col < (int)main_headers.size())
? main_headers[pick_left_col].c_str() : "?";
if (ImGui::BeginCombo("left col", lcur)) {
for (int k = 0; k < (int)main_headers.size(); ++k) {
bool sel = (k == pick_left_col);
if (ImGui::Selectable(main_headers[k].c_str(), sel)) pick_left_col = k;
}
ImGui::EndCombo();
}
// right col combo (de joinables[pick_source_idx].headers)
const TableInput& src = joinables[pick_source_idx];
const char* rcur = (pick_right_col >= 0 && pick_right_col < (int)src.headers.size())
? src.headers[pick_right_col].c_str() : "?";
ImGui::SetNextItemWidth(180);
if (ImGui::BeginCombo("right col", rcur)) {
for (int k = 0; k < (int)src.headers.size(); ++k) {
bool sel = (k == pick_right_col);
if (ImGui::Selectable(src.headers[k].c_str(), sel)) pick_right_col = k;
}
ImGui::EndCombo();
}
ImGui::Separator();
if (ImGui::SmallButton("Add")) {
Join jn;
jn.alias = pick_alias;
jn.source = src.name;
jn.on.push_back({main_headers[pick_left_col], src.headers[pick_right_col]});
jn.strategy = (JoinStrategy)pick_strategy;
st.joins.push_back(jn);
pick_alias[0] = 0;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::SmallButton("Cancel")) ImGui::CloseCurrentPopup();
ImGui::EndPopup();
}
ImGui::NewLine();
}
// ---------------------------------------------------------------------------
// Filter chips para el stage activo. eff_headers/eff_cols son del INPUT del
// stage activo (= orig+derived para stage 0; output del stage previo para 1+).
// ---------------------------------------------------------------------------
void draw_filter_chips(Stage& stg, const char* const* eff_headers, int eff_cols,
const std::vector<ColumnType>& eff_types) {
auto& U = ui();
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(120, 60, 170, 220));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(150, 85, 200, 240));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 95, 45, 140, 240));
if (ImGui::SmallButton("+##addfilter_btn")) ImGui::OpenPopup("##addfilter");
ImGui::PopStyleColor(3);
ImGui::SameLine();
// Presets (fase 10): menu con Last7/30/90d (cols Date), ExcludeNulls (any),
// NonZero (cols numericas). Apply append a stg.filters via build_preset_filters.
if (ImGui::SmallButton("Presets##fpresets")) ImGui::OpenPopup("##presets_menu");
if (ImGui::BeginPopup("##presets_menu")) {
int first_date = -1, first_num = -1;
for (int c = 0; c < eff_cols && c < (int)eff_types.size(); ++c) {
if (first_date < 0 && eff_types[c] == ColumnType::Date) first_date = c;
if (first_num < 0 && (eff_types[c] == ColumnType::Int ||
eff_types[c] == ColumnType::Float)) first_num = c;
}
auto apply_preset = [&](FilterPreset p, int col) {
auto fs = build_preset_filters(p, col, today_iso());
for (auto& f : fs) stg.filters.push_back(f);
};
if (first_date >= 0) {
char l1[96], l2[96], l3[96];
std::snprintf(l1, sizeof(l1), "Last 7 days on \"%s\"", eff_headers[first_date]);
std::snprintf(l2, sizeof(l2), "Last 30 days on \"%s\"", eff_headers[first_date]);
std::snprintf(l3, sizeof(l3), "Last 90 days on \"%s\"", eff_headers[first_date]);
if (ImGui::MenuItem(l1)) apply_preset(FilterPreset::Last7d, first_date);
if (ImGui::MenuItem(l2)) apply_preset(FilterPreset::Last30d, first_date);
if (ImGui::MenuItem(l3)) apply_preset(FilterPreset::Last90d, first_date);
ImGui::Separator();
}
if (ImGui::BeginMenu("Exclude nulls in...")) {
for (int c = 0; c < eff_cols; ++c) {
if (ImGui::MenuItem(eff_headers[c])) apply_preset(FilterPreset::ExcludeNulls, c);
}
ImGui::EndMenu();
}
if (first_num >= 0) {
if (ImGui::BeginMenu("Non-zero in...")) {
for (int c = 0; c < eff_cols && c < (int)eff_types.size(); ++c) {
if (eff_types[c] == ColumnType::Int || eff_types[c] == ColumnType::Float) {
if (ImGui::MenuItem(eff_headers[c])) apply_preset(FilterPreset::NonZero, c);
}
}
ImGui::EndMenu();
}
}
ImGui::EndPopup();
}
ImGui::SameLine();
if (stg.filters.empty()) {
ImGui::TextDisabled("Sin filtros.");
return;
}
for (size_t i = 0; i < stg.filters.size(); ) {
const auto& f = stg.filters[i];
const char* hdr = (f.col >= 0 && f.col < eff_cols) ? eff_headers[f.col] : "?";
char buf[256];
std::snprintf(buf, sizeof(buf), "%s %s %s x##chip%zu",
hdr, op_label(f.op), f.value.c_str(), i);
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(120, 60, 170, 220));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(150, 85, 200, 240));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 95, 45, 140, 240));
bool clicked = ImGui::SmallButton(buf);
ImGui::PopStyleColor(3);
// Click derecho: edit
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
U.edit_chip_kind = 1;
U.edit_chip_idx = (int)i;
U.edit_col_idx = f.col;
U.edit_op = (int)f.op;
U.edit_value = f.value;
ImGui::OpenPopup("##edit_filter");
}
if (clicked) { stg.filters.erase(stg.filters.begin() + i); continue; }
ImGui::SameLine();
++i;
}
ImGui::NewLine();
}
// Chips de breakout (stage > 0).
void draw_breakout_chips(Stage& stg, const char* const* in_headers, int in_cols,
const std::vector<ColumnType>& in_types) {
auto& U = ui();
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32( 60, 160, 170, 220));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32( 80, 190, 200, 240));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 40, 130, 140, 240));
if (ImGui::SmallButton("+##addbreakout_btn")) ImGui::OpenPopup("##addbreakout");
ImGui::PopStyleColor(3);
ImGui::SameLine();
if (stg.breakouts.empty()) {
ImGui::TextDisabled("Group by: ninguna col.");
return;
}
for (size_t i = 0; i < stg.breakouts.size(); ) {
std::string col_name;
DateGranularity g = parse_breakout_granularity(stg.breakouts[i], col_name);
// Resolve col index para lookup de tipo.
int col_idx = -1;
for (int c = 0; c < in_cols; ++c) {
if (std::strcmp(in_headers[c], col_name.c_str()) == 0) { col_idx = c; break; }
}
bool is_date_col = (col_idx >= 0 && col_idx < (int)in_types.size()
&& in_types[col_idx] == ColumnType::Date);
char buf[256];
std::snprintf(buf, sizeof(buf), "%s x##bk%zu", stg.breakouts[i].c_str(), i);
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32( 60, 160, 170, 220));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32( 80, 190, 200, 240));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 40, 130, 140, 240));
bool clicked = ImGui::SmallButton(buf);
ImGui::PopStyleColor(3);
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
U.edit_chip_kind = 2;
U.edit_chip_idx = (int)i;
U.edit_col_idx = (col_idx >= 0) ? col_idx : 0;
ImGui::OpenPopup("##edit_breakout");
}
if (clicked) { stg.breakouts.erase(stg.breakouts.begin() + i); continue; }
// Granularity combo inline cuando col Date (fase 10).
if (is_date_col) {
ImGui::SameLine();
const char* preview = (g == DateGranularity::None)
? "(raw)" : date_granularity_token(g);
char combo_id[32];
std::snprintf(combo_id, sizeof(combo_id), "##gran%zu", i);
ImGui::SetNextItemWidth(72);
if (ImGui::BeginCombo(combo_id, preview)) {
DateGranularity opts[] = {
DateGranularity::None,
DateGranularity::Year,
DateGranularity::Month,
DateGranularity::Week,
DateGranularity::Day,
DateGranularity::Hour,
};
for (auto o : opts) {
const char* lbl = (o == DateGranularity::None)
? "(raw)" : date_granularity_token(o);
if (ImGui::Selectable(lbl, o == g)) {
stg.breakouts[i] = compose_breakout(col_name, o);
}
}
ImGui::EndCombo();
}
}
ImGui::SameLine();
++i;
}
ImGui::NewLine();
}
const char* agg_fn_label(AggFn f) {
switch (f) {
case AggFn::Count: return "count";
case AggFn::Sum: return "sum";
case AggFn::Avg: return "avg";
case AggFn::Min: return "min";
case AggFn::Max: return "max";
case AggFn::Distinct: return "distinct";
case AggFn::Stddev: return "stddev";
case AggFn::Median: return "median";
case AggFn::P25: return "p25";
case AggFn::P75: return "p75";
case AggFn::P90: return "p90";
case AggFn::P99: return "p99";
case AggFn::Percentile: return "percentile";
}
return "?";
}
void draw_aggregation_chips(Stage& stg, const char* const* in_headers, int in_cols) {
auto& U = ui();
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32( 40, 140, 60, 220));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32( 60, 170, 85, 240));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 30, 110, 45, 240));
if (ImGui::SmallButton("+##addagg_btn")) ImGui::OpenPopup("##addagg");
ImGui::PopStyleColor(3);
ImGui::SameLine();
if (stg.aggregations.empty()) {
ImGui::TextDisabled("Aggregations: ninguna.");
return;
}
for (size_t i = 0; i < stg.aggregations.size(); ) {
const auto& a = stg.aggregations[i];
char buf[256];
if (a.fn == AggFn::Count) {
std::snprintf(buf, sizeof(buf), "count x##ag%zu", i);
} else if (a.fn == AggFn::Percentile) {
std::snprintf(buf, sizeof(buf), "percentile(%s, %g) x##ag%zu",
a.col.c_str(), a.arg, i);
} else {
std::snprintf(buf, sizeof(buf), "%s(%s) x##ag%zu",
agg_fn_label(a.fn), a.col.c_str(), i);
}
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32( 40, 140, 60, 220));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32( 60, 170, 85, 240));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 30, 110, 45, 240));
bool clicked = ImGui::SmallButton(buf);
ImGui::PopStyleColor(3);
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
U.edit_chip_kind = 3;
U.edit_chip_idx = (int)i;
U.edit_agg_fn = (int)a.fn;
U.edit_agg_arg = a.arg;
U.edit_col_idx = 0;
for (int c = 0; c < in_cols; ++c) {
if (std::strcmp(in_headers[c], a.col.c_str()) == 0) {
U.edit_col_idx = c; break;
}
}
ImGui::OpenPopup("##edit_agg");
}
if (clicked) { stg.aggregations.erase(stg.aggregations.begin() + i); continue; }
ImGui::SameLine();
++i;
}
(void)in_headers; (void)in_cols;
ImGui::NewLine();
}
void draw_sort_chips(Stage& stg) {
auto& U = ui();
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(220, 130, 50, 230));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(240, 155, 75, 245));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(180, 100, 30, 240));
if (ImGui::SmallButton("+##addsort_btn")) ImGui::OpenPopup("##addsort");
ImGui::PopStyleColor(3);
ImGui::SameLine();
if (stg.sorts.empty()) {
ImGui::TextDisabled("Sort: ninguno.");
return;
}
int erase_idx = -1;
int drag_src = -1;
int drag_dst = -1;
for (size_t i = 0; i < stg.sorts.size(); ++i) {
const auto& sc = stg.sorts[i];
char buf[256];
std::snprintf(buf, sizeof(buf), "%zu. %s %s x##srt%zu",
i + 1, sc.col.c_str(), sc.desc ? "desc" : "asc", i);
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(220, 130, 50, 230));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(240, 155, 75, 245));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(180, 100, 30, 240));
bool clicked = ImGui::SmallButton(buf);
ImGui::PopStyleColor(3);
// Drag source: prioridad multi-sort reorderable.
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) {
int idx = (int)i;
ImGui::SetDragDropPayload("##sortreorder", &idx, sizeof(int));
ImGui::Text("Move sort #%zu", i + 1);
ImGui::EndDragDropSource();
}
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* p = ImGui::AcceptDragDropPayload("##sortreorder")) {
drag_src = *(const int*)p->Data;
drag_dst = (int)i;
}
ImGui::EndDragDropTarget();
}
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
U.edit_chip_kind = 4;
U.edit_chip_idx = (int)i;
U.edit_value = sc.col;
U.edit_sort_desc = sc.desc;
ImGui::OpenPopup("##edit_sort");
}
if (clicked) erase_idx = (int)i;
ImGui::SameLine();
}
ImGui::NewLine();
if (drag_src >= 0 && drag_dst >= 0 && drag_src != drag_dst &&
drag_src < (int)stg.sorts.size() && drag_dst < (int)stg.sorts.size())
{
SortClause moved = std::move(stg.sorts[drag_src]);
stg.sorts.erase(stg.sorts.begin() + drag_src);
int insert_at = (drag_src < drag_dst) ? drag_dst : drag_dst;
if (insert_at > (int)stg.sorts.size()) insert_at = (int)stg.sorts.size();
stg.sorts.insert(stg.sorts.begin() + insert_at, std::move(moved));
} else if (erase_idx >= 0 && erase_idx < (int)stg.sorts.size()) {
stg.sorts.erase(stg.sorts.begin() + erase_idx);
}
}
// ---- Edit chip popups: click derecho sobre chip abre popup. ----
// Header click handler:
// click: si col ya esta en sorts -> cicla su direccion asc/desc/off.
// sino -> append {col, asc} al final (multi-sort por defecto).
// shift+click: reset. Reemplaza sorts con {col, asc} (sort unico).
void apply_header_sort_click(Stage& stg, const std::string& col_name, bool shift) {
if (shift) {
stg.sorts.clear();
stg.sorts.push_back({col_name, false});
return;
}
int idx = -1;
for (size_t i = 0; i < stg.sorts.size(); ++i) {
if (stg.sorts[i].col == col_name) { idx = (int)i; break; }
}
if (idx < 0) {
stg.sorts.push_back({col_name, false});
} else {
if (!stg.sorts[idx].desc) stg.sorts[idx].desc = true;
else stg.sorts.erase(stg.sorts.begin() + idx);
}
}
void draw_edit_filter_popup(Stage& stg, const char* const* headers, int n_cols,
const std::vector<ColumnType>& types) {
auto& U = ui();
if (!ImGui::BeginPopup("##edit_filter")) return;
if (U.edit_chip_idx >= 0 && U.edit_chip_idx < (int)stg.filters.size()) {
auto& f = stg.filters[U.edit_chip_idx];
ImGui::SetNextItemWidth(200);
const char* cur = (U.edit_col_idx >= 0 && U.edit_col_idx < n_cols)
? headers[U.edit_col_idx] : "?";
if (ImGui::BeginCombo("col", cur)) {
for (int c = 0; c < n_cols; ++c) {
bool sel = (U.edit_col_idx == c);
if (ImGui::Selectable(headers[c], sel)) U.edit_col_idx = c;
}
ImGui::EndCombo();
}
ColumnType t = (U.edit_col_idx >= 0 && U.edit_col_idx < (int)types.size())
? types[U.edit_col_idx] : ColumnType::String;
auto ops = ops_for_type(t);
ImGui::SetNextItemWidth(140);
if (ImGui::BeginCombo("op", op_label((Op)U.edit_op))) {
for (auto o : ops) {
bool sel = ((int)o == U.edit_op);
if (ImGui::Selectable(op_label(o), sel)) U.edit_op = (int)o;
}
ImGui::EndCombo();
}
char vbuf[256] = {0};
std::snprintf(vbuf, sizeof(vbuf), "%s", U.edit_value.c_str());
ImGui::SetNextItemWidth(220);
if (ImGui::InputText("value", vbuf, sizeof(vbuf))) U.edit_value = vbuf;
if (ImGui::Button("Save")) {
f.col = U.edit_col_idx;
f.op = (Op)U.edit_op;
f.value = U.edit_value;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel")) ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
void draw_edit_breakout_popup(Stage& stg, const char* const* headers, int n_cols) {
auto& U = ui();
if (!ImGui::BeginPopup("##edit_breakout")) return;
if (U.edit_chip_idx >= 0 && U.edit_chip_idx < (int)stg.breakouts.size()) {
ImGui::SetNextItemWidth(240);
const char* cur = (U.edit_col_idx >= 0 && U.edit_col_idx < n_cols)
? headers[U.edit_col_idx] : "?";
if (ImGui::BeginCombo("col", cur)) {
for (int c = 0; c < n_cols; ++c) {
bool sel = (U.edit_col_idx == c);
if (ImGui::Selectable(headers[c], sel)) U.edit_col_idx = c;
}
ImGui::EndCombo();
}
if (ImGui::Button("Save")) {
if (U.edit_col_idx >= 0 && U.edit_col_idx < n_cols)
stg.breakouts[U.edit_chip_idx] = headers[U.edit_col_idx];
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel")) ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
void draw_edit_agg_popup(Stage& stg, const char* const* headers, int n_cols) {
auto& U = ui();
if (!ImGui::BeginPopup("##edit_agg")) return;
if (U.edit_chip_idx >= 0 && U.edit_chip_idx < (int)stg.aggregations.size()) {
const AggFn all_fns[] = {AggFn::Count, AggFn::Sum, AggFn::Avg, AggFn::Min, AggFn::Max,
AggFn::Distinct, AggFn::Stddev, AggFn::Median,
AggFn::P25, AggFn::P75, AggFn::P90, AggFn::P99,
AggFn::Percentile};
ImGui::SetNextItemWidth(160);
if (ImGui::BeginCombo("fn", agg_fn_label((AggFn)U.edit_agg_fn))) {
for (auto f : all_fns) {
bool sel = ((int)f == U.edit_agg_fn);
if (ImGui::Selectable(agg_fn_label(f), sel)) U.edit_agg_fn = (int)f;
}
ImGui::EndCombo();
}
if ((AggFn)U.edit_agg_fn != AggFn::Count) {
ImGui::SetNextItemWidth(200);
const char* cur = (U.edit_col_idx >= 0 && U.edit_col_idx < n_cols)
? headers[U.edit_col_idx] : "?";
if (ImGui::BeginCombo("col", cur)) {
for (int c = 0; c < n_cols; ++c) {
bool sel = (U.edit_col_idx == c);
if (ImGui::Selectable(headers[c], sel)) U.edit_col_idx = c;
}
ImGui::EndCombo();
}
}
if ((AggFn)U.edit_agg_fn == AggFn::Percentile) {
float v = (float)U.edit_agg_arg;
ImGui::SetNextItemWidth(140);
if (ImGui::InputFloat("p (0..1)", &v, 0.05f, 0.1f, "%.2f"))
U.edit_agg_arg = v;
}
if (ImGui::Button("Save")) {
auto& a = stg.aggregations[U.edit_chip_idx];
a.fn = (AggFn)U.edit_agg_fn;
if (a.fn != AggFn::Count && U.edit_col_idx >= 0 && U.edit_col_idx < n_cols)
a.col = headers[U.edit_col_idx];
else if (a.fn == AggFn::Count) a.col.clear();
a.arg = U.edit_agg_arg;
a.alias.clear();
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel")) ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
void draw_edit_sort_popup(Stage& stg, const char* const* headers, int n_cols) {
auto& U = ui();
if (!ImGui::BeginPopup("##edit_sort")) return;
if (U.edit_chip_idx >= 0 && U.edit_chip_idx < (int)stg.sorts.size()) {
ImGui::SetNextItemWidth(240);
if (ImGui::BeginCombo("col", U.edit_value.c_str())) {
for (int c = 0; c < n_cols; ++c) {
bool sel = (U.edit_value == headers[c]);
if (ImGui::Selectable(headers[c], sel)) U.edit_value = headers[c];
}
ImGui::EndCombo();
}
if (ImGui::RadioButton("asc", !U.edit_sort_desc)) U.edit_sort_desc = false;
ImGui::SameLine();
if (ImGui::RadioButton("desc", U.edit_sort_desc)) U.edit_sort_desc = true;
if (ImGui::Button("Save")) {
auto& sc = stg.sorts[U.edit_chip_idx];
sc.col = U.edit_value;
sc.desc = U.edit_sort_desc;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel")) ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
void maybe_recompute_stats(const char* const* cells, int rows, int orig_cols,
int eff_cols, const std::vector<Filter>& filters,
const std::vector<int>& visible,
const std::vector<int>& src_for_eff)
{
auto& U = ui();
if (!U.stats_mode) return;
size_t fh = filters_hash(filters);
bool ds_changed = (cells != U.last_cells || rows != U.last_rows ||
eff_cols != U.last_eff_cols ||
(int)U.stats_cache.size() != eff_cols);
bool fl_changed = (fh != U.last_filter_h || (int)visible.size() != U.last_visible);
if (!ds_changed && !fl_changed) return;
U.stats_cache.resize(eff_cols);
const int* idx = visible.empty() ? nullptr : visible.data();
int n = (int)visible.size();
for (int c = 0; c < eff_cols; ++c) {
int src = src_for_eff[c];
U.stats_cache[c] = compute_column_stats(cells, rows, orig_cols, src,
100000, idx, n);
}
U.last_cells = cells;
U.last_rows = rows;
U.last_eff_cols = eff_cols;
U.last_filter_h = fh;
U.last_visible = (int)visible.size();
}
bool draw_typed_ops(ColumnType t, Op& out) {
auto ops = ops_for_type(t);
for (size_t i = 0; i < ops.size(); ++i) {
if (i % 5 != 0) ImGui::SameLine();
if (ImGui::SmallButton(op_label(ops[i]))) { out = ops[i]; return true; }
}
return false;
}
bool type_supports_range(ColumnType t) {
return t == ColumnType::Int || t == ColumnType::Float || t == ColumnType::Date;
}
void draw_add_filter_popup(Stage& stg, const char* const* eff_headers_arr, int eff_cols,
const std::vector<ColumnType>& eff_types)
{
auto& U = ui();
if (!ImGui::BeginPopup("##addfilter")) return;
if (U.addf_col < 0 || U.addf_col >= eff_cols) U.addf_col = 0;
ImGui::SetNextItemWidth(220);
if (ImGui::BeginCombo("col", eff_headers_arr[U.addf_col])) {
for (int c = 0; c < eff_cols; ++c) {
char it[160];
std::snprintf(it, sizeof(it), "%s %s",
column_type_icon(eff_types[c]), eff_headers_arr[c]);
bool sel = (U.addf_col == c);
if (ImGui::Selectable(it, sel)) U.addf_col = c;
}
ImGui::EndCombo();
}
ColumnType t = eff_types[U.addf_col];
ImGui::TextDisabled("type: %s %s", column_type_icon(t), column_type_name(t));
bool can_range = type_supports_range(t);
if (can_range) ImGui::Checkbox("Range (min/max)", &U.addf_range);
else U.addf_range = false;
if (!U.addf_range) {
char buf[256] = {0};
std::snprintf(buf, sizeof(buf), "%s", U.addf_val.c_str());
ImGui::SetNextItemWidth(220);
if (ImGui::InputText("val", buf, sizeof(buf))) U.addf_val = buf;
Op picked;
if (draw_typed_ops(t, picked)) {
stg.filters.push_back({U.addf_col, picked, U.addf_val});
U.addf_val.clear();
ImGui::CloseCurrentPopup();
}
} else {
char lo[128] = {0}, hi[128] = {0};
std::snprintf(lo, sizeof(lo), "%s", U.addf_lo.c_str());
std::snprintf(hi, sizeof(hi), "%s", U.addf_hi.c_str());
ImGui::SetNextItemWidth(100);
if (ImGui::InputText("min", lo, sizeof(lo))) U.addf_lo = lo;
ImGui::SameLine();
ImGui::SetNextItemWidth(100);
if (ImGui::InputText("max", hi, sizeof(hi))) U.addf_hi = hi;
ImGui::SameLine();
if (ImGui::SmallButton("Add range")) {
if (!U.addf_lo.empty()) stg.filters.push_back({U.addf_col, Op::Gte, U.addf_lo});
if (!U.addf_hi.empty()) stg.filters.push_back({U.addf_col, Op::Lte, U.addf_hi});
U.addf_lo.clear(); U.addf_hi.clear();
ImGui::CloseCurrentPopup();
}
}
ImGui::EndPopup();
}
void draw_add_breakout_popup(Stage& stg, const char* const* in_headers, int in_cols,
const std::vector<ColumnType>& in_types,
const char* const* in_cells, int in_rows) {
auto& U = ui();
if (!ImGui::BeginPopup("##addbreakout")) return;
if (U.brk_picker_col < 0 || U.brk_picker_col >= in_cols) U.brk_picker_col = 0;
ImGui::SetNextItemWidth(220);
if (ImGui::BeginCombo("col##bkcol", in_headers[U.brk_picker_col])) {
for (int c = 0; c < in_cols; ++c) {
char it[160];
std::snprintf(it, sizeof(it), "%s %s",
column_type_icon(in_types[c]), in_headers[c]);
bool sel = (U.brk_picker_col == c);
if (ImGui::Selectable(it, sel)) U.brk_picker_col = c;
}
ImGui::EndCombo();
}
if (ImGui::Button("Add##bk")) {
int c = U.brk_picker_col;
std::string col = in_headers[c];
// Fase 10: si col es Date, auto-detect granularidad via rango lexical
// (ISO YYYY-MM-DD ordena bien). Default Day si rango invalido.
if (c >= 0 && c < (int)in_types.size() && in_types[c] == ColumnType::Date) {
std::string lo, hi;
column_min_max(in_cells, in_rows, in_cols, c, lo, hi);
DateGranularity g = auto_date_granularity(lo, hi);
stg.breakouts.emplace_back(compose_breakout(col, g));
} else {
stg.breakouts.emplace_back(col);
}
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
void draw_add_aggregation_popup(Stage& stg, const char* const* in_headers, int in_cols,
const std::vector<ColumnType>& in_types) {
auto& U = ui();
if (!ImGui::BeginPopup("##addagg")) return;
AggFn cur_fn = (AggFn)U.agg_picker_fn;
ImGui::SetNextItemWidth(160);
if (ImGui::BeginCombo("fn##aggfn", agg_fn_label(cur_fn))) {
AggFn all[] = {AggFn::Count, AggFn::Sum, AggFn::Avg, AggFn::Min, AggFn::Max,
AggFn::Distinct, AggFn::Stddev, AggFn::Median,
AggFn::P25, AggFn::P75, AggFn::P90, AggFn::P99,
AggFn::Percentile};
for (AggFn f : all) {
bool sel = (f == cur_fn);
if (ImGui::Selectable(agg_fn_label(f), sel)) U.agg_picker_fn = (int)f;
}
ImGui::EndCombo();
}
if (cur_fn != AggFn::Count) {
if (U.agg_picker_col < 0 || U.agg_picker_col >= in_cols) U.agg_picker_col = 0;
ImGui::SetNextItemWidth(220);
if (ImGui::BeginCombo("col##aggcol", in_headers[U.agg_picker_col])) {
for (int c = 0; c < in_cols; ++c) {
char it[160];
std::snprintf(it, sizeof(it), "%s %s",
column_type_icon(in_types[c]), in_headers[c]);
bool sel = (U.agg_picker_col == c);
if (ImGui::Selectable(it, sel)) U.agg_picker_col = c;
}
ImGui::EndCombo();
}
}
if (cur_fn == AggFn::Percentile) {
double v = U.agg_picker_arg;
ImGui::SetNextItemWidth(120);
if (ImGui::InputDouble("p (0..1)", &v, 0.05, 0.1, "%.2f")) {
if (v < 0) v = 0; if (v > 1) v = 1;
U.agg_picker_arg = v;
}
}
if (ImGui::Button("Add##ag")) {
Aggregation a;
a.fn = cur_fn;
a.col = (cur_fn == AggFn::Count) ? "" : std::string(in_headers[U.agg_picker_col]);
a.arg = (cur_fn == AggFn::Percentile) ? U.agg_picker_arg : 0.0;
stg.aggregations.push_back(a);
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
void draw_add_sort_popup(Stage& stg, const char* const* in_headers, int in_cols,
const std::vector<ColumnType>& in_types) {
auto& U = ui();
if (!ImGui::BeginPopup("##addsort")) return;
if (U.sort_picker_col < 0 || U.sort_picker_col >= in_cols) U.sort_picker_col = 0;
ImGui::SetNextItemWidth(220);
if (ImGui::BeginCombo("col##sortcol", in_headers[U.sort_picker_col])) {
for (int c = 0; c < in_cols; ++c) {
char it[160];
std::snprintf(it, sizeof(it), "%s %s",
column_type_icon(in_types[c]), in_headers[c]);
bool sel = (U.sort_picker_col == c);
if (ImGui::Selectable(it, sel)) U.sort_picker_col = c;
}
ImGui::EndCombo();
}
ImGui::Checkbox("desc", &U.sort_picker_desc);
if (ImGui::Button("Add##srt")) {
SortClause sc;
sc.col = in_headers[U.sort_picker_col];
sc.desc = U.sort_picker_desc;
stg.sorts.push_back(sc);
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
void draw_header_menu(State& st, Stage& stg, int col,
const char* const* eff_headers_arr, int eff_cols,
const std::vector<ColumnType>& eff_types,
int orig_cols, bool is_raw_stage)
{
auto& U = ui();
ColumnType t = eff_types[col];
if (ImGui::MenuItem("Sort ascending")) {
stg.sorts.clear();
stg.sorts.push_back({eff_headers_arr[col], false});
}
if (ImGui::MenuItem("Sort descending")) {
stg.sorts.clear();
stg.sorts.push_back({eff_headers_arr[col], true});
}
if (!stg.sorts.empty() && ImGui::MenuItem("Clear sort")) stg.sorts.clear();
ImGui::Separator();
auto& fbuf = U.filter_inputs[col];
fbuf.resize(256, '\0');
if (ImGui::BeginMenu("Filter...")) {
ImGui::SetNextItemWidth(220);
ImGui::InputText("##filterval", fbuf.data(), fbuf.size());
std::string val(fbuf.c_str());
auto ops = ops_for_type(t);
for (size_t i = 0; i < ops.size(); ++i) {
if (i % 5 != 0) ImGui::SameLine();
if (ImGui::SmallButton(op_label(ops[i]))) {
stg.filters.push_back({col, ops[i], val});
ImGui::CloseCurrentPopup();
}
}
ImGui::EndMenu();
}
// Change type / derived solo en stage 0.
if (is_raw_stage) {
if (ImGui::BeginMenu("Change type")) {
const ColumnType types[] = {
ColumnType::String, ColumnType::Int, ColumnType::Float,
ColumnType::Bool, ColumnType::Date, ColumnType::Json
};
for (auto nt : types) {
char lab[64];
std::snprintf(lab, sizeof(lab), "%s %s",
column_type_icon(nt), column_type_name(nt));
if (ImGui::MenuItem(lab)) {
DerivedColumn d;
d.source_col = (col < orig_cols) ? col : stg.derived[col - orig_cols].source_col;
d.type = nt;
d.name = std::string(eff_headers_arr[col]) + "_" + column_type_name(nt);
stg.derived.push_back(d);
}
}
ImGui::EndMenu();
}
}
if (ImGui::BeginMenu("Conditional color")) {
auto& vbuf = U.color_value_inputs[col];
vbuf.resize(256, '\0');
if (U.color_picker_vals.find(col) == U.color_picker_vals.end())
U.color_picker_vals[col] = ImVec4(0.85f, 0.40f, 0.30f, 0.60f);
ImVec4& cv = U.color_picker_vals[col];
ImGui::SetNextItemWidth(180);
ImGui::InputText("equals", vbuf.data(), vbuf.size());
ImGui::ColorEdit4("color", &cv.x, ImGuiColorEditFlags_NoInputs);
if (ImGui::Button("Apply")) {
ImU32 c = ImGui::ColorConvertFloat4ToU32(cv);
st.color_rules.push_back({col, std::string(vbuf.c_str()), (unsigned int)c});
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Clear col")) {
for (size_t i = 0; i < st.color_rules.size();) {
if (st.color_rules[i].col == col) st.color_rules.erase(st.color_rules.begin() + i);
else ++i;
}
}
ImGui::EndMenu();
}
if (ImGui::MenuItem("Hide column")) st.col_visible[col] = false;
if (is_raw_stage && col >= orig_cols && ImGui::MenuItem("Remove derived column")) {
int k = col - orig_cols;
stg.derived.erase(stg.derived.begin() + k);
}
ImGui::Separator();
if (ImGui::BeginMenu("Columns")) {
for (int k = 0; k < eff_cols; ++k) {
bool v = st.col_visible[k];
char lab[160];
std::snprintf(lab, sizeof(lab), "%s %s",
column_type_icon(eff_types[k]), eff_headers_arr[k]);
if (ImGui::Checkbox(lab, &v)) st.col_visible[k] = v;
}
if (ImGui::MenuItem("Show all")) {
for (int k = 0; k < eff_cols; ++k) st.col_visible[k] = true;
}
ImGui::EndMenu();
}
}
// ---------------------------------------------------------------------------
// Drill-down: anade un filter al stage previo y cambia active a stage previo.
// `col_name` y `value` se aplican como un Filter Op::Eq sobre el stage N-1.
// ---------------------------------------------------------------------------
void drill_into(State& st, int from_stage,
const std::string& col_name, const std::string& value,
const std::vector<std::string>& prev_input_headers)
{
if (from_stage <= 0 || from_stage >= (int)st.stages.size()) return;
int target = from_stage - 1;
int ci = -1;
for (size_t i = 0; i < prev_input_headers.size(); ++i) {
if (prev_input_headers[i] == col_name) { ci = (int)i; break; }
}
if (ci < 0) return;
// Fase 10: graba step en drill_back, limpia forward (rama nueva).
DrillStep step;
step.target_stage = target;
step.filter_pos = (int)st.stages[target].filters.size();
step.prev_active_stage = st.active_stage;
step.added = make_drill_filter(ci, value);
apply_drill_step(st, step);
auto& U = ui();
U.drill_back.push_back(step);
U.drill_forward.clear();
}
} // anon namespace
void render(const char* id,
const std::vector<TableInput>& tables,
State& st,
std::vector<TableEvent>* events_out,
bool show_chrome)
{
if (tables.empty()) return;
int main_idx = resolve_main_idx(tables, st.main_source);
if (main_idx < 0) return;
// Merge aux_column_specs from State into TableInput when the caller passed
// empty column_specs. Caller-provided specs always take precedence.
// We keep a local copy to avoid mutating the caller's const tables.
static thread_local TableInput main_t_merged;
{
const TableInput& src = tables[(size_t)main_idx];
if (src.column_specs.empty() &&
main_idx < (int)st.aux_column_specs.size() &&
!st.aux_column_specs[(size_t)main_idx].empty())
{
main_t_merged = src;
main_t_merged.column_specs = st.aux_column_specs[(size_t)main_idx];
} else {
main_t_merged = src;
}
}
const TableInput& main_t = main_t_merged;
static thread_local std::vector<const char*> main_hdr_ptrs;
main_hdr_ptrs.clear();
main_hdr_ptrs.reserve(main_t.cols);
for (int c = 0; c < main_t.cols; ++c) main_hdr_ptrs.push_back(main_t.headers[c].c_str());
const char* const* headers_in = main_hdr_ptrs.data();
int col_count = main_t.cols;
const char* const* cells_in = main_t.cells;
int row_count_in = main_t.rows;
const ColumnType* declared_types_in = main_t.types.data();
// Joinables = todas las demas tablas.
static thread_local std::vector<TableInput> joinables_v;
joinables_v.clear();
for (int i = 0; i < (int)tables.size(); ++i) {
if (i != main_idx) joinables_v.push_back(tables[(size_t)i]);
}
const std::vector<TableInput>* joinables = joinables_v.empty() ? nullptr : &joinables_v;
// Per-table chrome visibility (issue: previously global in UiCache → flipping
// one table's "Show UI" affected all tables on screen). Now lives in State.
bool chrome_visible = st.chrome_user_set ? st.chrome_user_visible : show_chrome;
// Toggle Hide/Show UI siempre visible (botoncito arriba a la derecha).
{
float right = ImGui::GetWindowContentRegionMax().x;
ImGui::SetCursorPosX(right - 90.0f);
if (ImGui::SmallButton(chrome_visible ? "Hide UI##chrome" : "Show UI##chrome")) {
st.chrome_user_set = true;
st.chrome_user_visible = !chrome_visible;
}
}
// Main source dropdown — solo si > 1 tabla disponibles.
if (chrome_visible && tables.size() > 1) {
ImGui::SameLine();
float right = ImGui::GetWindowContentRegionMax().x;
ImGui::SetCursorPosX(right - 90.0f - 280.0f);
ImGui::TextDisabled("Main table:");
ImGui::SameLine();
ImGui::SetNextItemWidth(180);
const char* cur_main = main_t.name.c_str();
if (ImGui::BeginCombo("##main_table", cur_main)) {
for (const auto& t : tables) {
bool sel = (t.name == cur_main);
if (ImGui::Selectable(t.name.c_str(), sel)) {
st.main_source = t.name;
}
}
ImGui::EndCombo();
}
}
st.ensure_stage0();
// -------- Pre-pipeline: materialize joins --------
// Si state.joins no vacio + joinables provistos, ejecutar chain de join_tables.
// El resultado reemplaza headers/cells/declared_types para el resto del render.
static thread_local std::vector<std::string> joined_headers_store;
static thread_local std::vector<ColumnType> joined_types_store;
static thread_local std::vector<const char*> joined_headers_ptrs;
static thread_local std::vector<const char*> joined_cells_ptrs;
static thread_local std::vector<ColumnType> joined_declared_types;
static thread_local StageOutput joined_so;
const char* const* headers = headers_in;
const char* const* cells = cells_in;
int row_count = row_count_in;
int orig_cols = col_count;
const ColumnType* declared_types = declared_types_in;
bool joined = false;
if (!st.joins.empty() && joinables && !joinables->empty()) {
joined_so = StageOutput{};
// Build initial left from main.
std::vector<std::string> cur_h(orig_cols);
std::vector<ColumnType> cur_t(orig_cols);
for (int c = 0; c < orig_cols; ++c) {
cur_h[c] = headers_in[c];
cur_t[c] = declared_types_in ? declared_types_in[c] : ColumnType::Auto;
}
const char* const* cur_cells = cells_in;
int cur_rows = row_count_in;
int cur_cols = orig_cols;
// Chain join por cada joins[i].
std::vector<StageOutput> chain;
chain.reserve(st.joins.size());
for (const auto& jn : st.joins) {
const TableInput* match = nullptr;
for (const auto& ti : *joinables) {
if (ti.name == jn.source) { match = &ti; break; }
}
if (!match) continue;
StageOutput so = join_tables(cur_cells, cur_rows, cur_cols,
cur_h, cur_t, *match, jn);
chain.push_back(std::move(so));
const StageOutput& last = chain.back();
cur_cells = last.cells.data();
cur_rows = last.rows;
cur_cols = last.cols;
cur_h = last.headers;
cur_t = last.types;
}
if (!chain.empty()) {
joined = true;
joined_so = std::move(chain.back());
joined_headers_store = joined_so.headers;
joined_types_store = joined_so.types;
joined_headers_ptrs.clear();
joined_cells_ptrs.clear();
for (const auto& s : joined_headers_store) joined_headers_ptrs.push_back(s.c_str());
for (const auto& s : joined_so.cell_backing) joined_cells_ptrs.push_back(s.c_str());
joined_declared_types = joined_types_store;
headers = joined_headers_ptrs.data();
cells = joined_cells_ptrs.data();
row_count = joined_so.rows;
orig_cols = joined_so.cols;
declared_types = joined_declared_types.data();
}
}
Stage& stage0 = st.stages[0];
int eff_cols = orig_cols + (int)stage0.derived.size();
ensure_init(st, eff_cols);
auto& U = ui();
// Build eff_headers / src_for_eff / eff_types para STAGE 0.
std::vector<const char*> eff_headers(eff_cols);
std::vector<int> src_for_eff(eff_cols);
std::vector<ColumnType> eff_types(eff_cols);
for (int c = 0; c < eff_cols; ++c) {
if (c < orig_cols) {
eff_headers[c] = headers[c];
src_for_eff[c] = c;
ColumnType d = declared_types ? declared_types[c] : ColumnType::Auto;
eff_types[c] = effective_type(d, cells, row_count, orig_cols, c);
} else {
const DerivedColumn& d = stage0.derived[c - orig_cols];
eff_headers[c] = d.name.c_str();
src_for_eff[c] = d.source_col;
eff_types[c] = d.type;
}
}
static thread_local std::vector<std::string> hn_storage;
static thread_local std::unordered_map<std::string, int> name_to_col;
static thread_local std::unordered_map<std::string, int> derived_n2i;
hn_storage.clear();
name_to_col.clear();
derived_n2i.clear();
hn_storage.reserve(orig_cols);
for (int c = 0; c < orig_cols; ++c) {
hn_storage.emplace_back(headers[c]);
name_to_col[hn_storage.back()] = c;
}
for (int i = 0; i < (int)stage0.derived.size(); ++i) {
derived_n2i[stage0.derived[i].name] = i;
}
// Re-fit auto en cambio de display, stage o config.
auto hash_cfg = [](const ViewConfig& c) -> size_t {
std::string s = c.x_col + "|" + c.cat_col + "|" + c.size_col;
for (auto& y : c.y_cols) { s += "|"; s += y; }
s += "|"; s += std::to_string(c.primary_color);
s += "|"; s += std::to_string(c.hist_bins);
s += "|"; s += std::to_string(c.pie_radius);
s += "|"; s += c.show_legend ? "1" : "0";
s += "|"; s += c.show_markers ? "1" : "0";
return std::hash<std::string>{}(s);
};
size_t cur_cfg_h = hash_cfg(st.viz_config);
if (U.prev_viz_display != st.display || U.prev_viz_stage != st.active_stage ||
U.prev_viz_cfg_h != cur_cfg_h) {
st.viz_config.fit_request = true;
U.prev_viz_display = st.display;
U.prev_viz_stage = st.active_stage;
U.prev_viz_cfg_h = cur_cfg_h;
}
// ----- Breadcrumb + viz selector (chrome) -----
if (chrome_visible) {
draw_stage_breadcrumb(st);
draw_viz_selector(st);
}
int active = st.active_stage;
bool is_raw = (active == 0);
// ----- Chips del stage activo -----
Stage& act = st.stages[active];
if (is_raw && chrome_visible) {
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 2));
// Joins chip row — solo si hay joinables disponibles.
if (joinables && !joinables->empty()) {
std::vector<std::string> mh(orig_cols);
for (int c = 0; c < orig_cols; ++c) mh[c] = headers[c];
draw_joins_chips(st, *joinables, mh);
}
draw_filter_chips(act, eff_headers.data(), eff_cols, eff_types);
draw_add_filter_popup(act, eff_headers.data(), eff_cols, eff_types);
draw_edit_filter_popup(act, eff_headers.data(), eff_cols, eff_types);
// Custom columns chips (solo stage 0)
{
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(110, 110, 110, 200));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(140, 140, 140, 230));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 85, 85, 85, 230));
if (ImGui::SmallButton("+##addcustomcol")) {
U.cf_open = true;
U.cf_editing = false;
U.cf_edit_idx = -1;
U.cf_target_stage = 0;
U.cf_formula.clear();
U.cf_name.clear();
U.cf_type = ColumnType::String;
U.cf_error.clear();
}
ImGui::PopStyleColor(3);
ImGui::SameLine();
bool any = false;
for (size_t i = 0; i < stage0.derived.size(); ++i) {
if (stage0.derived[i].formula.empty()) continue;
any = true;
const auto& d = stage0.derived[i];
char buf[256];
std::snprintf(buf, sizeof(buf), "%s %s x##custom%zu",
column_type_icon(d.type), d.name.c_str(), i);
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(140, 140, 140, 220));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(170, 170, 170, 240));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(110, 110, 110, 240));
bool clicked = ImGui::SmallButton(buf);
ImGui::PopStyleColor(3);
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
U.cf_open = true;
U.cf_editing = true;
U.cf_edit_idx = (int)i;
U.cf_target_stage = 0;
U.cf_formula = d.formula;
U.cf_name = d.name;
U.cf_type = d.type;
U.cf_error.clear();
}
if (clicked) {
if (d.lua_id >= 0) lua_engine::release(lua_engine::get(), d.lua_id);
stage0.derived.erase(stage0.derived.begin() + i);
break;
}
ImGui::SameLine();
}
if (!any) ImGui::TextDisabled("Custom columns: + para anadir.");
else ImGui::NewLine();
}
// Sort chips para stage 0 (input headers para popup).
draw_sort_chips(act);
draw_add_sort_popup(act, eff_headers.data(), eff_cols, eff_types);
draw_edit_sort_popup(act, eff_headers.data(), eff_cols);
ImGui::PopStyleVar(); // ItemSpacing
}
// Para stages 1+, compute input headers/types del stage previo.
// Esto requiere compute_stage chain. Lo haremos abajo.
// ---------- Compute view: chain compute_stage 0..active ----------
// Stage 0 expressions: derived cols. Pero compute_stage no sabe de Lua.
// Estrategia: stage 0 lo aplicamos a mano (orig cells + filter + sort)
// y exponemos un eff_cells "virtual" donde derived cols se llenan via Lua
// en el render. Esto preserva el path actual.
//
// Para stages 1+, compute_stage opera sobre cells materializadas. Hay que
// materializar el stage 0 output como cells reales (con derived evaluadas).
// Simpler: si active == 0, mantener el path actual (orig cells + Lua).
// Si active > 0, materializar stage 0 + chain compute_stage(stage 1..active).
if (is_raw) {
// ----- Path stage 0: orig cells + filters/sort manuales + Lua per cell.
// compute_visible_rows opera sobre orig cells. filter.col es eff col,
// hay que traducir a src col (igual que codigo anterior).
State st_tmp = st;
st_tmp.ensure_stage0();
for (auto& f : st_tmp.stages[0].filters) {
if (f.col >= 0 && f.col < eff_cols) f.col = src_for_eff[f.col];
}
// Sort: la pasamos por @idx convention.
st_tmp.stages[0].sorts.clear();
if (!stage0.sorts.empty()) {
// resolve col name -> col idx (de eff_cols) -> src
const SortClause& sc0 = stage0.sorts.front();
int sc_eff = -1;
for (int c = 0; c < eff_cols; ++c) {
if (std::strcmp(eff_headers[c], sc0.col.c_str()) == 0) { sc_eff = c; break; }
}
if (sc_eff >= 0) {
int sc_src = src_for_eff[sc_eff];
char tmp[16]; std::snprintf(tmp, sizeof(tmp), "@%d", sc_src);
st_tmp.stages[0].sorts.push_back({tmp, sc0.desc});
}
}
auto visible_rows = compute_visible_rows(cells, row_count, orig_cols, st_tmp);
int visible_cols = 0;
for (int k = 0; k < eff_cols; ++k) if (st.col_visible[k]) ++visible_cols;
// Snapshot del active output (stage 0) para el config popup.
U.active_headers.clear();
U.active_types.clear();
for (int k = 0; k < eff_cols; ++k) {
if (!st.col_visible[k]) continue;
U.active_headers.emplace_back(eff_headers[k]);
U.active_types.push_back(eff_types[k]);
}
// Input == orig + derived (stage 0 no tiene upstream que agrupe).
U.input_headers_active = U.active_headers;
U.input_types_active = U.active_types;
if (chrome_visible)
{
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 2));
ImGui::Text("Filas: %d / %d Columnas: %d / %d",
(int)visible_rows.size(), row_count, visible_cols, eff_cols);
ImGui::SameLine();
if (ImGui::SmallButton(U.stats_mode ? "Hide stats" : "Show stats")) {
U.stats_mode = !U.stats_mode;
}
ImGui::SameLine();
if (ImGui::SmallButton("Export CSV")) {
std::string out;
bool first = true;
for (int oc = 0; oc < (int)st.col_order.size(); ++oc) {
int c = st.col_order[oc];
if (c < 0 || c >= eff_cols) continue;
if (!st.col_visible[c]) continue;
if (!first) out += ',';
out += csv_escape(eff_headers[c]);
first = false;
}
out += '\n';
for (int r : visible_rows) {
first = true;
for (int oc = 0; oc < (int)st.col_order.size(); ++oc) {
int c = st.col_order[oc];
if (c < 0 || c >= eff_cols) continue;
if (!st.col_visible[c]) continue;
int src = src_for_eff[c];
if (!first) out += ',';
out += csv_escape(cells[r * orig_cols + src]);
first = false;
}
out += '\n';
}
const char* p = fn::local_path("export_table.csv");
std::ofstream f(p, std::ios::binary | std::ios::trunc);
if (f) { f << out; U.last_export_path = p; }
}
if (!U.last_export_path.empty()) {
ImGui::SameLine();
ImGui::TextDisabled("-> %s", U.last_export_path.c_str());
}
ImGui::SameLine();
if (ImGui::SmallButton("Show TQL")) {
std::vector<std::string> orig_headers(orig_cols);
std::vector<ColumnType> orig_types(orig_cols);
for (int c = 0; c < orig_cols; ++c) {
orig_headers[c] = headers[c];
orig_types[c] = eff_types[c];
}
U.tql_show_text = tql::emit(st, orig_headers, orig_types);
U.tql_show_open = true;
}
ImGui::SameLine();
if (ImGui::SmallButton("Apply TQL")) {
U.tql_apply_open = true;
U.tql_apply_error.clear();
}
ImGui::SameLine();
ImGui::SetNextItemWidth(160);
ImGui::InputText("##tql_file", U.tql_file_path, sizeof(U.tql_file_path));
ImGui::SameLine();
if (ImGui::SmallButton("Save .tql")) {
std::vector<std::string> oh(orig_cols);
std::vector<ColumnType> ot(orig_cols);
for (int c = 0; c < orig_cols; ++c) { oh[c] = headers[c]; ot[c] = eff_types[c]; }
std::string text = tql::emit(st, oh, ot);
const char* path = fn::local_path(U.tql_file_path);
std::ofstream f(path);
if (f) { f << text; U.tql_io_status = std::string("saved: ") + path; }
else { U.tql_io_status = std::string("save FAILED: ") + path; }
}
ImGui::SameLine();
if (ImGui::SmallButton("Load .tql")) {
const char* path = fn::local_path(U.tql_file_path);
std::ifstream f(path);
if (!f) { U.tql_io_status = std::string("load FAILED: ") + path; }
else {
std::string text((std::istreambuf_iterator<char>(f)),
std::istreambuf_iterator<char>());
std::vector<std::string> oh(orig_cols);
std::vector<ColumnType> ot(orig_cols);
for (int c = 0; c < orig_cols; ++c) { oh[c] = headers[c]; ot[c] = eff_types[c]; }
std::string err;
bool ok = tql::apply(text, st, oh, ot, cells, row_count, orig_cols, &err);
if (ok) U.tql_io_status = std::string("loaded: ") + path + (err.empty() ? "" : " (warn: " + err + ")");
else U.tql_io_status = std::string("load parse error: ") + err;
}
}
if (!U.tql_io_status.empty()) {
ImGui::SameLine();
ImGui::TextDisabled("%s", U.tql_io_status.c_str());
}
ImGui::PopStyleVar();
} // chrome_visible
maybe_recompute_stats(cells, row_count, orig_cols, eff_cols,
st_tmp.stages[0].filters,
visible_rows, src_for_eff);
// Toggle Table <-> View: solo visible cuando NO estamos en Table.
// Desde la tabla no se ofrece volver a chart (la tabla es estado
// canonico final). Cambia display via menu/chips si quieres ver chart.
if (st.display != ViewMode::Table) {
draw_table_toggle(st.display, U.last_non_table_main, "main", &st);
}
// SO compartido: main viz + extras. Construido on-demand.
StageOutput so_main;
bool so_built = false;
auto build_so = [&]() -> StageOutput& {
if (so_built) return so_main;
so_built = true;
std::vector<int> vcols;
for (int c = 0; c < eff_cols; ++c) if (st.col_visible[c]) vcols.push_back(c);
so_main.cols = (int)vcols.size();
so_main.rows = (int)visible_rows.size();
so_main.headers.reserve(so_main.cols);
so_main.types.reserve(so_main.cols);
for (int c : vcols) {
so_main.headers.emplace_back(eff_headers[c]);
so_main.types.push_back(eff_types[c]);
}
so_main.cell_backing.reserve((size_t)so_main.rows * so_main.cols);
for (int r : visible_rows) {
for (int c : vcols) {
if (c < orig_cols) {
const char* p = cells[r * orig_cols + c];
so_main.cell_backing.emplace_back(p ? p : "");
} else {
const DerivedColumn& d = stage0.derived[c - orig_cols];
if (!d.formula.empty() && d.lua_id >= 0) {
lua_engine::RowCtx ctx;
ctx.cells = cells;
ctx.orig_cols = orig_cols;
ctx.row = r;
ctx.header_names = &hn_storage;
ctx.name_to_col = &name_to_col;
ctx.types_orig = eff_types.data();
ctx.n_types_orig = orig_cols;
ctx.derived = &stage0.derived;
ctx.derived_name_to_idx = &derived_n2i;
std::string err;
so_main.cell_backing.emplace_back(
lua_engine::eval(lua_engine::get(), d.lua_id, ctx, &err));
} else {
int src = d.source_col;
const char* sp = (src >= 0 && src < orig_cols) ? cells[r * orig_cols + src] : "";
so_main.cell_backing.emplace_back(sp ? sp : "");
}
}
}
}
so_main.cells.reserve(so_main.cell_backing.size());
for (auto& s : so_main.cell_backing) so_main.cells.push_back(s.c_str());
return so_main;
};
if (visible_cols == 0) {
ImGui::TextDisabled("(todas las columnas ocultas)");
// Modales fuera del table block.
} else if (st.display != ViewMode::Table) {
viz::render(build_so(), st.display, st.viz_config, ImVec2(-1, -1));
} else {
ImGuiTableFlags flags =
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY;
// Issue 0081-O.7/9: disable ALL Selectable bg painting. Hover + selection
// are painted via TableSetBgColor(CellBg, ...) below, edge-to-edge.
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0));
if (ImGui::BeginTable(id, visible_cols, flags, ImVec2(0, 0))) {
for (int dc = 0; dc < (int)st.col_order.size(); ++dc) {
int c = st.col_order[dc];
if (c < 0 || c >= eff_cols) continue;
if (!st.col_visible[c]) continue;
ImGui::TableSetupColumn(eff_headers[c], ImGuiTableColumnFlags_None, 0.0f, (ImGuiID)c);
}
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableNextRow(ImGuiTableRowFlags_Headers);
for (int dc = 0; dc < (int)st.col_order.size(); ++dc) {
int c = st.col_order[dc];
if (c < 0 || c >= eff_cols) continue;
if (!st.col_visible[c]) continue;
ImGui::TableSetColumnIndex(dc); // visual idx aprox; recomputado por engine
ImGui::PushID(c);
// Detecta si esta col esta en sorts (primario o secundario)
int sort_pos = -1;
bool sort_desc = false;
for (size_t si = 0; si < act.sorts.size(); ++si) {
if (act.sorts[si].col == eff_headers[c]) {
sort_pos = (int)si; sort_desc = act.sorts[si].desc; break;
}
}
char arrow[16] = "";
if (sort_pos == 0) std::snprintf(arrow, sizeof(arrow), " %s", sort_desc ? "v" : "^");
else if (sort_pos > 0) std::snprintf(arrow, sizeof(arrow), " %s%d", sort_desc ? "v" : "^", sort_pos + 1);
char label[200];
std::snprintf(label, sizeof(label), "%s %s%s",
column_type_icon(eff_types[c]), eff_headers[c], arrow);
ImGui::PushStyleColor(ImGuiCol_Header, IM_COL32(45, 50, 65, 200));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, IM_COL32(65, 75, 95, 220));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, IM_COL32(80, 95, 130, 240));
bool clicked = ImGui::Selectable(label, false, ImGuiSelectableFlags_DontClosePopups);
ImGui::PopStyleColor(3);
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) {
ImGui::SetDragDropPayload("##colreorder", &c, sizeof(int));
ImGui::Text("Move %s", eff_headers[c]);
ImGui::EndDragDropSource();
}
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* p = ImGui::AcceptDragDropPayload("##colreorder")) {
int src = *(const int*)p->Data;
reorder_column(st, src, c);
}
ImGui::EndDragDropTarget();
}
if (clicked) {
bool shift = ImGui::GetIO().KeyShift;
apply_header_sort_click(act, eff_headers[c], shift);
}
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
U.header_popup_col = c;
ImGui::OpenPopup("##hdr_menu");
}
if (ImGui::BeginPopup("##hdr_menu") && U.header_popup_col == c) {
draw_header_menu(st, act, c, eff_headers.data(), eff_cols, eff_types, orig_cols, true);
ImGui::EndPopup();
}
if (U.stats_mode && c < (int)U.stats_cache.size()) {
const ColStats& s = U.stats_cache[c];
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(170, 190, 220, 220));
ImGui::Text("missing: %d", s.empty_count);
ImGui::Text("uniq: %d%s", s.unique_count, s.unique_capped ? "+" : "");
if (s.numeric) {
ImGui::Text("mean: %.2f", s.mean);
ImGui::Text("p25: %.2f", s.p25);
ImGui::Text("p50: %.2f", s.p50);
ImGui::Text("p75: %.2f", s.p75);
if (!s.hist.empty()) {
char overlay[64];
std::snprintf(overlay, sizeof(overlay), "[%.2f..%.2f]", s.min, s.max);
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, IM_COL32(70, 140, 220, 230));
ImGui::PlotHistogram("##hist", s.hist.data(), (int)s.hist.size(),
0, overlay, 0.0f, FLT_MAX, ImVec2(-1, 36));
ImGui::PopStyleColor();
}
} else if (!s.top_categories.empty()) {
int mx = 0;
for (const auto& kv : s.top_categories) if (kv.second > mx) mx = kv.second;
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, IM_COL32(70, 140, 220, 220));
for (const auto& kv : s.top_categories) {
float frac = mx > 0 ? (float)kv.second / (float)mx : 0.f;
char ovl[96];
std::snprintf(ovl, sizeof(ovl), "%s (%d)", kv.first.c_str(), kv.second);
ImGui::ProgressBar(frac, ImVec2(-1, 12), ovl);
}
ImGui::PopStyleColor();
}
ImGui::PopStyleColor();
}
ImGui::PopID();
}
int sel_rmin = std::min(U.sel_anchor_row, U.sel_end_row);
int sel_rmax = std::max(U.sel_anchor_row, U.sel_end_row);
int sel_cmin = std::min(U.sel_anchor_col, U.sel_end_col);
int sel_cmax = std::max(U.sel_anchor_col, U.sel_end_col);
ImGuiListClipper clipper;
clipper.Begin((int)visible_rows.size());
while (clipper.Step()) {
for (int ri = clipper.DisplayStart; ri < clipper.DisplayEnd; ++ri) {
int r = visible_rows[ri];
ImGui::TableNextRow();
int draw_idx = 0;
for (int oc = 0; oc < (int)st.col_order.size(); ++oc) {
int c = st.col_order[oc];
if (c < 0 || c >= eff_cols) continue;
if (!st.col_visible[c]) continue;
ImGui::TableSetColumnIndex(draw_idx++);
// Issue 0081-O.7: capture cell rect before rendering for
// per-cell hover overlay (manual hit-test avoids relying on
// GetItemRect of the last sub-item inside draw_cell_custom).
ImVec2 cell_min = ImGui::GetCursorScreenPos();
float cell_w = ImGui::GetContentRegionAvail().x;
float cell_h = ImGui::GetTextLineHeight();
ImVec2 cell_max(cell_min.x + cell_w, cell_min.y + cell_h);
int src = src_for_eff[c];
std::string eval_buf;
const char* cell;
if (c >= orig_cols && !stage0.derived[c - orig_cols].formula.empty()) {
const auto& d = stage0.derived[c - orig_cols];
if (d.lua_id < 0) cell = "?";
else {
lua_engine::RowCtx ctx;
ctx.cells = cells;
ctx.orig_cols = orig_cols;
ctx.row = r;
ctx.header_names = &hn_storage;
ctx.name_to_col = &name_to_col;
ctx.types_orig = eff_types.data();
ctx.n_types_orig = orig_cols;
ctx.derived = &stage0.derived;
ctx.derived_name_to_idx = &derived_n2i;
std::string err;
eval_buf = lua_engine::eval(lua_engine::get(), d.lua_id, ctx, &err);
cell = eval_buf.c_str();
}
} else {
cell = cells[r * orig_cols + src];
}
for (const auto& cr : st.color_rules) {
if (cr.col == c && cell && cr.equals == cell) {
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, (ImU32)cr.color);
break;
}
}
bool in_sel = (U.sel_active &&
ri >= sel_rmin && ri <= sel_rmax &&
oc >= sel_cmin && oc <= sel_cmax);
ImGui::PushID(r * eff_cols + c);
// Issue 0081-N/O: use declarative renderer when column_specs set.
{
bool custom_rendered = false;
const ColumnSpec* cell_cs = nullptr;
if (!main_t.column_specs.empty() &&
c < (int)main_t.column_specs.size()) {
cell_cs = &main_t.column_specs[(size_t)c];
if (cell_cs->renderer != CellRenderer::Text) {
draw_cell_custom(*cell_cs, cell, r, c, events_out);
custom_rendered = true;
}
}
if (!custom_rendered) {
// Issue 0081-O.8: disable Selectable's own bg paint so
// it doesn't double-up with the manual cell overlay below.
// Pass explicit size so empty cells still have a hit area
// (otherwise drag-select skips empty cells).
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0, 0, 0, 0));
ImGui::Selectable(cell ? cell : "", in_sel,
ImGuiSelectableFlags_AllowDoubleClick,
ImVec2(0, ImGui::GetTextLineHeight()));
ImGui::PopStyleColor();
// Tooltip for Text cells (Phase 2).
if (cell_cs && cell_cs->tooltip_on_hover &&
ImGui::IsItemHovered()) {
const char* tip = (cell_cs->tooltip == "auto")
? (cell ? cell : "")
: cell_cs->tooltip.c_str();
if (tip && tip[0]) ImGui::SetTooltip("%s", tip);
}
}
}
// Issue 0081-O.7/9/10: per-cell bg via TableSetBgColor.
// Edge-to-edge (full cell incl. CellPadding). Selection (in_sel)
// overrides hover. Both colors uniform — no Selectable padding.
{
ImVec2 mp = ImGui::GetMousePos();
ImVec2 pad = ImGui::GetStyle().CellPadding;
bool is_hovered =
mp.x >= cell_min.x - pad.x && mp.x < cell_min.x + cell_w + pad.x &&
mp.y >= cell_min.y - pad.y && mp.y < cell_min.y + cell_h + pad.y &&
ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
if (in_sel) {
// Selected (drag-range): blue, slightly stronger when hovered.
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg,
is_hovered
? IM_COL32(102, 140, 217, 80)
: IM_COL32(102, 140, 217, 60));
} else if (is_hovered) {
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg,
IM_COL32(255, 255, 255, 22));
}
}
// AllowWhenBlockedByActiveItem: durante drag,
// otras celdas tambien reciben hover -> sel se
// pinta mientras arrastras.
bool hovered = ImGui::IsItemHovered(
ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
if (hovered) {
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
U.sel_anchor_row = ri; U.sel_anchor_col = oc;
U.sel_end_row = ri; U.sel_end_col = oc;
U.sel_active = true;
U.sel_dragging = true;
} else if (U.sel_dragging) {
U.sel_end_row = ri; U.sel_end_col = oc;
}
// RowDoubleClick event (Phase 2, v1.2.0).
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)
&& events_out) {
TableEvent ev;
ev.kind = TableEventKind::RowDoubleClick;
ev.row = r;
ev.col = c;
ev.value = cell ? cell : "";
if (!main_t.column_specs.empty() &&
c < (int)main_t.column_specs.size())
ev.column_id = main_t.column_specs[(size_t)c].id;
events_out->push_back(std::move(ev));
}
// RowRightClick event: emit event only, no popup drawn here.
// Caller inspects events_out and opens its own context menu.
if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
U.pending_col = c;
U.pending_value = cell ? cell : "";
U.open_cell_popup = true;
if (events_out) {
TableEvent ev;
ev.kind = TableEventKind::RowRightClick;
ev.row = r;
ev.col = c;
ev.value = cell ? cell : "";
if (!main_t.column_specs.empty() &&
c < (int)main_t.column_specs.size())
ev.column_id = main_t.column_specs[(size_t)c].id;
events_out->push_back(std::move(ev));
}
}
}
ImGui::PopID();
}
}
}
if (U.sel_dragging && !ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
U.sel_dragging = false;
}
ImGui::EndTable();
}
ImGui::PopStyleColor(3); // HeaderHovered/HeaderActive/Header (issue 0081-O.7)
// Ctrl+C -> TSV.
if (U.sel_active && ImGui::GetIO().KeyCtrl &&
ImGui::IsKeyPressed(ImGuiKey_C, false))
{
int rmin = std::min(U.sel_anchor_row, U.sel_end_row);
int rmax = std::max(U.sel_anchor_row, U.sel_end_row);
int cmin = std::min(U.sel_anchor_col, U.sel_end_col);
int cmax = std::max(U.sel_anchor_col, U.sel_end_col);
std::string out;
bool first = true;
for (int oc = cmin; oc <= cmax; ++oc) {
if (oc < 0 || oc >= (int)st.col_order.size()) continue;
int c = st.col_order[oc];
if (c < 0 || c >= eff_cols) continue;
if (!st.col_visible[c]) continue;
if (!first) out += '\t';
out += eff_headers[c];
first = false;
}
out += '\n';
for (int ri = rmin; ri <= rmax; ++ri) {
if (ri < 0 || ri >= (int)visible_rows.size()) continue;
int r = visible_rows[ri];
first = true;
for (int oc = cmin; oc <= cmax; ++oc) {
if (oc < 0 || oc >= (int)st.col_order.size()) continue;
int c = st.col_order[oc];
if (c < 0 || c >= eff_cols) continue;
if (!st.col_visible[c]) continue;
int src = src_for_eff[c];
const char* v = cells[r * orig_cols + src];
std::string sv = v ? v : "";
for (char& ch : sv) if (ch == '\t' || ch == '\n') ch = ' ';
if (!first) out += '\t';
out += sv;
first = false;
}
out += '\n';
}
ImGui::SetClipboardText(out.c_str());
}
}
// Render extras panels (stage 0 path). Solo cuando display != Table —
// desde la tabla no se muestran chart panels adicionales.
if (st.display != ViewMode::Table && !st.extra_panels.empty() && visible_cols > 0) {
int close_idx = -1;
const std::vector<ColumnSpec>* ep_specs =
main_t.column_specs.empty() ? nullptr : &main_t.column_specs;
for (int i = 0; i < (int)st.extra_panels.size(); ++i) {
if (draw_extra_panel(st, st.extra_panels[i], i, build_so(), ep_specs)) close_idx = i;
}
if (close_idx >= 0) st.extra_panels.erase(st.extra_panels.begin() + close_idx);
}
} else {
// ----- Path stage > 0: materializar stage 0 con cells reales + chain.
// Materializar stage 0: aplicar filters/sort sobre orig + evaluar derived.
State st_tmp = st;
for (auto& f : st_tmp.stages[0].filters) {
if (f.col >= 0 && f.col < eff_cols) f.col = src_for_eff[f.col];
}
st_tmp.stages[0].sorts.clear();
if (!stage0.sorts.empty()) {
const SortClause& sc0 = stage0.sorts.front();
int sc_eff = -1;
for (int c = 0; c < eff_cols; ++c) {
if (std::strcmp(eff_headers[c], sc0.col.c_str()) == 0) { sc_eff = c; break; }
}
if (sc_eff >= 0) {
int sc_src = src_for_eff[sc_eff];
char tmp[16]; std::snprintf(tmp, sizeof(tmp), "@%d", sc_src);
st_tmp.stages[0].sorts.push_back({tmp, sc0.desc});
}
}
auto vrows = compute_visible_rows(cells, row_count, orig_cols, st_tmp);
// Materializar stage0 output: cells (eff_cols) con derived evaluadas.
std::vector<std::string> mat_backing;
std::vector<const char*> mat_cells;
mat_backing.reserve((size_t)vrows.size() * eff_cols);
mat_cells.reserve((size_t)vrows.size() * eff_cols);
for (int r : vrows) {
for (int c = 0; c < eff_cols; ++c) {
const char* p;
std::string buf;
if (c < orig_cols) {
p = cells[r * orig_cols + c];
mat_backing.emplace_back(p ? p : "");
} else {
const DerivedColumn& d = stage0.derived[c - orig_cols];
if (!d.formula.empty()) {
if (d.lua_id < 0) {
mat_backing.emplace_back("");
} else {
lua_engine::RowCtx ctx;
ctx.cells = cells;
ctx.orig_cols = orig_cols;
ctx.row = r;
ctx.header_names = &hn_storage;
ctx.name_to_col = &name_to_col;
ctx.types_orig = eff_types.data();
ctx.n_types_orig = orig_cols;
ctx.derived = &stage0.derived;
ctx.derived_name_to_idx = &derived_n2i;
std::string err;
mat_backing.emplace_back(
lua_engine::eval(lua_engine::get(), d.lua_id, ctx, &err));
}
} else {
// retipo puro
int src = d.source_col;
const char* sp = (src >= 0 && src < orig_cols) ? cells[r * orig_cols + src] : "";
mat_backing.emplace_back(sp ? sp : "");
}
}
}
}
// Punteros tras llenar backing (reserve garantiza no realloc).
for (auto& s : mat_backing) mat_cells.push_back(s.c_str());
std::vector<std::string> cur_headers(eff_cols);
std::vector<ColumnType> cur_types(eff_cols);
for (int c = 0; c < eff_cols; ++c) {
cur_headers[c] = eff_headers[c];
cur_types[c] = eff_types[c];
}
// Chain compute_stage 1..active.
// Para encadenar, mantenemos vectores por iteracion. cur_cells apunta al
// ultimo output.
const char* const* cur_cells = mat_cells.data();
int cur_rows = (int)vrows.size();
int cur_cols_n = eff_cols;
std::vector<StageOutput> outs;
outs.reserve(st.stages.size());
// Headers del INPUT del active (= output del active-1)
std::vector<std::string> input_headers_active = cur_headers;
std::vector<ColumnType> input_types_active = cur_types;
for (int si = 1; si <= active; ++si) {
const Stage& sN = st.stages[si];
// Antes de computar: si es el active stage, los input_headers son cur_*.
if (si == active) {
input_headers_active = cur_headers;
input_types_active = cur_types;
}
StageOutput so = compute_stage(cur_cells, cur_rows, cur_cols_n,
cur_headers, cur_types, sN);
outs.push_back(std::move(so));
const StageOutput& last = outs.back();
cur_cells = last.cells.data();
cur_rows = last.rows;
cur_cols_n = last.cols;
cur_headers = last.headers;
cur_types = last.types;
}
// ----- Chips del active stage (uses input_headers_active) -----
std::vector<const char*> ih_ptrs(input_headers_active.size());
for (size_t i = 0; i < input_headers_active.size(); ++i)
ih_ptrs[i] = input_headers_active[i].c_str();
int in_cols_n = (int)input_headers_active.size();
if (chrome_visible) {
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 2));
draw_filter_chips(act, ih_ptrs.data(), in_cols_n, input_types_active);
draw_add_filter_popup(act, ih_ptrs.data(), in_cols_n, input_types_active);
draw_edit_filter_popup(act, ih_ptrs.data(), in_cols_n, input_types_active);
draw_breakout_chips(act, ih_ptrs.data(), in_cols_n, input_types_active);
draw_add_breakout_popup(act, ih_ptrs.data(), in_cols_n, input_types_active,
cur_cells, cur_rows);
draw_edit_breakout_popup(act, ih_ptrs.data(), in_cols_n);
draw_aggregation_chips(act, ih_ptrs.data(), in_cols_n);
draw_add_aggregation_popup(act, ih_ptrs.data(), in_cols_n, input_types_active);
draw_edit_agg_popup(act, ih_ptrs.data(), in_cols_n);
// ----- Custom column chips (stages 1+, target = active stage) -----
{
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(110, 110, 110, 200));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(140, 140, 140, 230));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 85, 85, 85, 230));
if (ImGui::SmallButton("+##addcustomcol_stage")) {
U.cf_open = true;
U.cf_editing = false;
U.cf_edit_idx = -1;
U.cf_target_stage = active;
U.cf_formula.clear();
U.cf_name.clear();
U.cf_type = ColumnType::String;
U.cf_error.clear();
}
ImGui::PopStyleColor(3);
ImGui::SameLine();
bool any = false;
for (size_t i = 0; i < act.derived.size(); ++i) {
if (act.derived[i].formula.empty()) continue;
any = true;
const auto& d = act.derived[i];
char buf[256];
std::snprintf(buf, sizeof(buf), "%s %s x##custom_st_%zu",
column_type_icon(d.type), d.name.c_str(), i);
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(140, 140, 140, 220));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(170, 170, 170, 240));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(110, 110, 110, 240));
bool clicked = ImGui::SmallButton(buf);
ImGui::PopStyleColor(3);
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
U.cf_open = true;
U.cf_editing = true;
U.cf_edit_idx = (int)i;
U.cf_target_stage = active;
U.cf_formula = d.formula;
U.cf_name = d.name;
U.cf_type = d.type;
U.cf_error.clear();
}
if (clicked) {
if (d.lua_id >= 0) lua_engine::release(lua_engine::get(), d.lua_id);
act.derived.erase(act.derived.begin() + i);
break;
}
ImGui::SameLine();
}
if (!any) ImGui::TextDisabled("Custom columns (stage %d): + para anadir.", active);
else ImGui::NewLine();
}
draw_sort_chips(act);
// Sort col options son los headers del OUTPUT del stage activo.
std::vector<const char*> out_h_ptrs(cur_headers.size());
for (size_t i = 0; i < cur_headers.size(); ++i) out_h_ptrs[i] = cur_headers[i].c_str();
draw_add_sort_popup(act, out_h_ptrs.data(), (int)cur_headers.size(), cur_types);
draw_edit_sort_popup(act, out_h_ptrs.data(), (int)cur_headers.size());
ImGui::PopStyleVar();
} // chrome_visible
// ----- Materializar act.derived sobre cur_cells -----
// Para cada derived col formula del active stage, eval per output row.
std::vector<std::string> ext_backing;
std::vector<const char*> ext_cells;
std::vector<std::string> ext_headers;
std::vector<ColumnType> ext_types;
if (!act.derived.empty()) {
int orig_out_cols = cur_cols_n;
std::vector<std::string> out_hn = cur_headers;
std::unordered_map<std::string, int> out_n2c;
for (size_t i = 0; i < out_hn.size(); ++i) out_n2c[out_hn[i]] = (int)i;
int n_derived = (int)act.derived.size();
int new_cols = orig_out_cols + n_derived;
ext_backing.reserve((size_t)cur_rows * n_derived);
ext_cells.reserve((size_t)cur_rows * new_cols);
for (int r = 0; r < cur_rows; ++r) {
// copia cols originales del output
for (int c = 0; c < orig_out_cols; ++c) {
ext_cells.push_back(cur_cells[r * orig_out_cols + c]);
}
// anade derived eval
for (int k = 0; k < n_derived; ++k) {
const DerivedColumn& d = act.derived[k];
if (d.formula.empty() || d.lua_id < 0) {
ext_backing.emplace_back("");
} else {
lua_engine::RowCtx ctx;
ctx.cells = cur_cells;
ctx.orig_cols = orig_out_cols;
ctx.row = r;
ctx.header_names = &out_hn;
ctx.name_to_col = &out_n2c;
ctx.types_orig = cur_types.data();
ctx.n_types_orig = orig_out_cols;
std::string e;
ext_backing.emplace_back(
lua_engine::eval(lua_engine::get(), d.lua_id, ctx, &e));
}
// marker placeholder; sera replaced abajo tras backing estable
ext_cells.push_back(nullptr);
}
}
// Construir ext_cells reemplazando placeholders por punteros estables.
size_t bi = 0;
for (int r = 0; r < cur_rows; ++r) {
for (int k = 0; k < n_derived; ++k) {
int idx = r * new_cols + orig_out_cols + k;
ext_cells[idx] = ext_backing[bi++].c_str();
}
}
ext_headers = cur_headers;
ext_types = cur_types;
for (int k = 0; k < n_derived; ++k) {
ext_headers.push_back(act.derived[k].name);
ext_types.push_back(act.derived[k].type);
}
cur_cells = ext_cells.data();
cur_cols_n = new_cols;
cur_headers = ext_headers;
cur_types = ext_types;
}
// Header row + cells render simple (sin clipper porque outputs son
// pequenos tipicamente).
// Snapshot del active output (stage>0) para config popup.
U.active_headers = cur_headers;
U.active_types = cur_types;
// Input del active stage = output del previo. Disponible en
// input_headers_active/input_types_active.
U.input_headers_active = input_headers_active;
U.input_types_active = input_types_active;
if (chrome_visible) {
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 2));
ImGui::Text("Filas: %d Columnas: %d", cur_rows, cur_cols_n);
ImGui::SameLine();
if (ImGui::SmallButton(U.stats_mode ? "Hide stats" : "Show stats")) {
U.stats_mode = !U.stats_mode;
}
// Recompute stats sobre cur_cells del stage activo.
if (U.stats_mode && cur_cols_n > 0) {
U.stats_cache.resize(cur_cols_n);
U.last_cells = cur_cells;
for (int c = 0; c < cur_cols_n; ++c) {
U.stats_cache[c] = compute_column_stats(cur_cells, cur_rows, cur_cols_n, c);
}
}
ImGui::SameLine();
if (ImGui::SmallButton("Show TQL")) {
std::vector<std::string> oh(orig_cols);
std::vector<ColumnType> ot(orig_cols);
for (int c = 0; c < orig_cols; ++c) {
oh[c] = headers[c];
ot[c] = eff_types[c];
}
U.tql_show_text = tql::emit(st, oh, ot);
U.tql_show_open = true;
}
ImGui::SameLine();
if (ImGui::SmallButton("Apply TQL")) {
U.tql_apply_open = true;
U.tql_apply_error.clear();
}
ImGui::SameLine();
ImGui::SetNextItemWidth(160);
ImGui::InputText("##tql_file2", U.tql_file_path, sizeof(U.tql_file_path));
ImGui::SameLine();
if (ImGui::SmallButton("Save .tql##s2")) {
std::vector<std::string> oh(orig_cols);
std::vector<ColumnType> ot(orig_cols);
for (int c = 0; c < orig_cols; ++c) { oh[c] = headers[c]; ot[c] = eff_types[c]; }
std::string text = tql::emit(st, oh, ot);
const char* path = fn::local_path(U.tql_file_path);
std::ofstream f(path);
if (f) { f << text; U.tql_io_status = std::string("saved: ") + path; }
else { U.tql_io_status = std::string("save FAILED: ") + path; }
}
ImGui::SameLine();
if (ImGui::SmallButton("Load .tql##l2")) {
const char* path = fn::local_path(U.tql_file_path);
std::ifstream f(path);
if (!f) { U.tql_io_status = std::string("load FAILED: ") + path; }
else {
std::string text((std::istreambuf_iterator<char>(f)),
std::istreambuf_iterator<char>());
std::vector<std::string> oh(orig_cols);
std::vector<ColumnType> ot(orig_cols);
for (int c = 0; c < orig_cols; ++c) { oh[c] = headers[c]; ot[c] = eff_types[c]; }
std::string err;
bool ok = tql::apply(text, st, oh, ot, cells, row_count, orig_cols, &err);
if (ok) U.tql_io_status = std::string("loaded: ") + path + (err.empty() ? "" : " (warn: " + err + ")");
else U.tql_io_status = std::string("load parse error: ") + err;
}
}
if (!U.tql_io_status.empty()) {
ImGui::SameLine();
ImGui::TextDisabled("%s", U.tql_io_status.c_str());
}
ImGui::PopStyleVar();
} // chrome_visible
// Toggle Table <-> View: solo visible cuando NO estamos en Table.
if (st.display != ViewMode::Table) {
draw_table_toggle(st.display, U.last_non_table_main, "main2", &st);
}
if (st.display != ViewMode::Table && cur_cols_n > 0) {
// outs.back() es el StageOutput del active. Si active no tiene outs
// (cur_rows poblado pero outs vacio cuando active>0 y chain corta),
// construir uno on-the-fly desde cur_cells.
StageOutput so_local;
const StageOutput* so_ptr = nullptr;
if (!outs.empty()) {
so_ptr = &outs.back();
} else {
so_local.cols = cur_cols_n;
so_local.rows = cur_rows;
so_local.headers = cur_headers;
so_local.types = cur_types;
so_local.cells.reserve((size_t)cur_rows * cur_cols_n);
for (int i = 0; i < cur_rows * cur_cols_n; ++i)
so_local.cells.push_back(cur_cells[i]);
so_ptr = &so_local;
}
int clicked_row = -1;
viz::render(*so_ptr, st.display, st.viz_config, ImVec2(-1, -1), &clicked_row);
// Fase 10: click sobre chart -> drill al stage previo usando
// breakout col[0] como filtro Op::Eq sobre cells[clicked_row].
if (clicked_row >= 0 && active > 0 &&
so_ptr->cols > 0 && clicked_row < so_ptr->rows) {
int n_brk = (int)st.stages[active].breakouts.size();
if (n_brk > 0) {
const char* v = so_ptr->cells[clicked_row * so_ptr->cols + 0];
std::string col_clean;
parse_breakout_granularity(so_ptr->headers[0], col_clean);
drill_into(st, active, col_clean,
v ? std::string(v) : "",
input_headers_active);
}
}
goto stage_n_table_end;
}
{
ImGuiTableFlags flags =
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY;
// Issue 0081-O.7: tone down row hover (default 0.31 alpha was too bright).
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(1.0f, 1.0f, 1.0f, 0.05f));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(1.0f, 1.0f, 1.0f, 0.08f));
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.40f, 0.55f, 0.85f, 0.20f));
if (cur_cols_n > 0 && ImGui::BeginTable(id, cur_cols_n, flags, ImVec2(0, 0))) {
for (int c = 0; c < cur_cols_n; ++c) {
ImGui::TableSetupColumn(cur_headers[c].c_str(),
ImGuiTableColumnFlags_None, 0.0f, (ImGuiID)c);
}
ImGui::TableSetupScrollFreeze(0, 1);
// Custom header row: nombre + icon + stats inline si stats_mode.
ImGui::TableNextRow(ImGuiTableRowFlags_Headers);
for (int c = 0; c < cur_cols_n; ++c) {
ImGui::TableSetColumnIndex(c);
// Sort indicator
int sort_pos = -1;
bool sort_desc = false;
for (size_t si = 0; si < act.sorts.size(); ++si) {
if (act.sorts[si].col == cur_headers[c]) {
sort_pos = (int)si; sort_desc = act.sorts[si].desc; break;
}
}
char arrow[16] = "";
if (sort_pos == 0) std::snprintf(arrow, sizeof(arrow), " %s", sort_desc ? "v" : "^");
else if (sort_pos > 0) std::snprintf(arrow, sizeof(arrow), " %s%d", sort_desc ? "v" : "^", sort_pos + 1);
ImGui::PushStyleColor(ImGuiCol_Header, IM_COL32(45, 50, 65, 200));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, IM_COL32(65, 75, 95, 220));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, IM_COL32(80, 95, 130, 240));
char lbl[200];
std::snprintf(lbl, sizeof(lbl), "%s %s%s",
column_type_icon(cur_types[c]),
cur_headers[c].c_str(), arrow);
bool h_clicked = ImGui::Selectable(lbl, false, ImGuiSelectableFlags_DontClosePopups);
ImGui::PopStyleColor(3);
if (h_clicked) {
bool shift = ImGui::GetIO().KeyShift;
apply_header_sort_click(act, cur_headers[c], shift);
}
if (U.stats_mode && c < (int)U.stats_cache.size()) {
const ColStats& s = U.stats_cache[c];
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(170, 190, 220, 220));
ImGui::Text("missing: %d", s.empty_count);
ImGui::Text("uniq: %d%s", s.unique_count, s.unique_capped ? "+" : "");
if (s.numeric) {
ImGui::Text("mean: %.2f", s.mean);
ImGui::Text("p25: %.2f", s.p25);
ImGui::Text("p50: %.2f", s.p50);
ImGui::Text("p75: %.2f", s.p75);
if (!s.hist.empty()) {
char overlay[64];
std::snprintf(overlay, sizeof(overlay), "[%.2f..%.2f]", s.min, s.max);
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, IM_COL32(70, 140, 220, 230));
ImGui::PlotHistogram("##hist", s.hist.data(), (int)s.hist.size(),
0, overlay, 0.0f, FLT_MAX, ImVec2(-1, 36));
ImGui::PopStyleColor();
}
} else if (!s.top_categories.empty()) {
int mx = 0;
for (const auto& kv : s.top_categories) if (kv.second > mx) mx = kv.second;
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, IM_COL32(70, 140, 220, 220));
for (const auto& kv : s.top_categories) {
float frac = mx > 0 ? (float)kv.second / (float)mx : 0.f;
char ovl[96];
std::snprintf(ovl, sizeof(ovl), "%s (%d)", kv.first.c_str(), kv.second);
ImGui::ProgressBar(frac, ImVec2(-1, 12), ovl);
}
ImGui::PopStyleColor();
}
ImGui::PopStyleColor();
}
}
int n_brk = (int)st.stages[active].breakouts.size();
for (int r = 0; r < cur_rows; ++r) {
ImGui::TableNextRow();
for (int c = 0; c < cur_cols_n; ++c) {
ImGui::TableSetColumnIndex(c);
const char* cell = cur_cells[r * cur_cols_n + c];
ImGui::PushID(r * cur_cols_n + c);
// Issue 0081-N/O: declarative renderer for aggregated stage tables.
{
bool custom_rendered = false;
const ColumnSpec* cell_cs2 = nullptr;
if (!main_t.column_specs.empty() &&
c < (int)main_t.column_specs.size()) {
cell_cs2 = &main_t.column_specs[(size_t)c];
if (cell_cs2->renderer != CellRenderer::Text) {
draw_cell_custom(*cell_cs2, cell, r, c, events_out);
custom_rendered = true;
}
}
if (!custom_rendered) {
// Issue 0081-O.8: disable Selectable's bg paint to avoid
// double hover with the manual cell overlay; explicit size
// ensures empty cells have a hit area.
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0, 0, 0, 0));
ImGui::Selectable(cell ? cell : "", false, 0,
ImVec2(0, ImGui::GetTextLineHeight()));
ImGui::PopStyleColor();
// Tooltip for Text cells (Phase 2).
if (cell_cs2 && cell_cs2->tooltip_on_hover &&
ImGui::IsItemHovered()) {
const char* tip = (cell_cs2->tooltip == "auto")
? (cell ? cell : "")
: cell_cs2->tooltip.c_str();
if (tip && tip[0]) ImGui::SetTooltip("%s", tip);
}
}
}
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
U.pending_col = c;
U.pending_value = cell ? cell : "";
U.inspect_row = r;
ImGui::OpenPopup("##drill_popup");
// RowRightClick event (Phase 2, v1.2.0).
if (events_out) {
TableEvent ev;
ev.kind = TableEventKind::RowRightClick;
ev.row = r;
ev.col = c;
ev.value = cell ? cell : "";
if (!main_t.column_specs.empty() &&
c < (int)main_t.column_specs.size())
ev.column_id = main_t.column_specs[(size_t)c].id;
events_out->push_back(std::move(ev));
}
}
if (ImGui::BeginPopup("##drill_popup")) {
if (c < n_brk) {
char lbl[256];
std::snprintf(lbl, sizeof(lbl), "Drill into: %s = %s",
cur_headers[c].c_str(), cell ? cell : "");
if (ImGui::MenuItem(lbl)) {
drill_into(st, active, cur_headers[c],
cell ? std::string(cell) : "",
input_headers_active);
ImGui::CloseCurrentPopup();
}
ImGui::Separator();
}
if (ImGui::MenuItem("Inspect row...")) {
U.inspect_row = r;
U.inspect_open = true;
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
ImGui::PopID();
}
}
ImGui::EndTable();
}
ImGui::PopStyleColor(3); // HeaderHovered/HeaderActive/Header (issue 0081-O.7)
}
stage_n_table_end:;
// Row inspector modal (fase 10). Activado via right-click "Inspect row..."
// sobre celdas del table del stage activo. `cur_cells` ya es row-major.
draw_row_inspector_modal(st, active, cur_cells, cur_rows, cur_cols_n,
cur_headers, cur_types, input_headers_active);
// Render extras (stage>0 path). Solo cuando display != Table.
if (st.display != ViewMode::Table && !st.extra_panels.empty() && cur_cols_n > 0) {
StageOutput so_local;
const StageOutput* so_ptr = nullptr;
if (!outs.empty()) {
so_ptr = &outs.back();
} else {
so_local.cols = cur_cols_n;
so_local.rows = cur_rows;
so_local.headers = cur_headers;
so_local.types = cur_types;
so_local.cells.reserve((size_t)cur_rows * cur_cols_n);
for (int i = 0; i < cur_rows * cur_cols_n; ++i)
so_local.cells.push_back(cur_cells[i]);
so_ptr = &so_local;
}
int close_idx = -1;
const std::vector<ColumnSpec>* ep_specs2 =
main_t.column_specs.empty() ? nullptr : &main_t.column_specs;
for (int i = 0; i < (int)st.extra_panels.size(); ++i) {
if (draw_extra_panel(st, st.extra_panels[i], i, *so_ptr, ep_specs2)) close_idx = i;
}
if (close_idx >= 0) st.extra_panels.erase(st.extra_panels.begin() + close_idx);
}
}
// ---------- Modales (comunes a ambos paths) ----------
if (U.cf_open) ImGui::OpenPopup("Custom column");
if (ImGui::BeginPopupModal("Custom column", &U.cf_open,
ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::Text("Nombre:");
char name_buf[128] = {0};
std::snprintf(name_buf, sizeof(name_buf), "%s", U.cf_name.c_str());
ImGui::SetNextItemWidth(520);
if (ImGui::InputText("##cfname", name_buf, sizeof(name_buf))) U.cf_name = name_buf;
ImGui::Spacing();
ImGui::Text("Formula (Lua). Acceso celdas via row.<col_name> o row[idx].");
ImGui::TextDisabled("Ejemplo: return row.size_kb * 1024");
static char formula_buf[4096] = {0};
if (U.cf_force_cursor || std::strcmp(formula_buf, U.cf_formula.c_str()) != 0) {
std::snprintf(formula_buf, sizeof(formula_buf), "%s", U.cf_formula.c_str());
}
ImGuiInputTextFlags flags =
ImGuiInputTextFlags_CallbackEdit | ImGuiInputTextFlags_CallbackAlways;
if (ImGui::InputTextMultiline("##cfformula", formula_buf, sizeof(formula_buf),
ImVec2(520, 200), flags, autocomplete_cb, &U)) {
U.cf_formula = formula_buf;
}
if (U.cf_ac_open) {
ImVec2 box_min = ImGui::GetItemRectMin();
ImVec2 box_max = ImGui::GetItemRectMax();
ImGui::SetNextWindowPos(ImVec2(box_min.x + 20, box_max.y + 4));
ImGui::SetNextWindowSize(ImVec2(280, 0));
ImGuiWindowFlags wf =
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_AlwaysAutoResize;
if (ImGui::Begin("##colpicker", nullptr, wf)) {
ImGui::TextDisabled("Pick column:");
ImGui::Separator();
auto ci_contains = [](const std::string& hay, const std::string& nd) {
if (nd.empty()) return true;
std::string a = hay, b = nd;
for (char& c : a) if (c >= 'A' && c <= 'Z') c += 32;
for (char& c : b) if (c >= 'A' && c <= 'Z') c += 32;
return a.find(b) != std::string::npos;
};
int shown = 0;
for (int c = 0; c < eff_cols && shown < 12; ++c) {
std::string nm = eff_headers[c];
if (!ci_contains(nm, U.cf_ac_filter)) continue;
char lbl[200];
std::snprintf(lbl, sizeof(lbl), "%s %s",
column_type_icon(eff_types[c]), nm.c_str());
if (ImGui::Selectable(lbl)) {
int new_cursor = 0;
std::string updated = insert_column_ref(
U.cf_formula, U.cf_ac_start, U.cf_ac_cursor, nm, new_cursor);
U.cf_formula = updated;
U.cf_target_cursor= new_cursor;
U.cf_force_cursor = true;
U.cf_ac_open = false;
}
++shown;
}
if (shown == 0) ImGui::TextDisabled("(sin matches)");
}
ImGui::End();
}
if (!U.cf_error.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(230, 100, 100, 255));
ImGui::TextWrapped("Error: %s", U.cf_error.c_str());
ImGui::PopStyleColor();
}
if (ImGui::Button("Compile & save")) {
std::string err;
int lid = lua_engine::compile(lua_engine::get(), U.cf_formula, &err);
if (lid < 0) {
U.cf_error = err;
} else {
// Build sample context segun cf_target_stage.
// target == 0: usa orig cells + stage 0 derived.
// target > 0: recomputa chain hasta el target (excluyendo
// derived del target) y sample sobre ese output.
int ts = U.cf_target_stage;
if (ts < 0 || ts >= (int)st.stages.size()) ts = 0;
int sample = 0;
std::vector<std::string> samples_str;
if (ts == 0) {
sample = std::min(64, row_count);
for (int r = 0; r < sample; ++r) {
lua_engine::RowCtx ctx;
ctx.cells = cells;
ctx.orig_cols = orig_cols;
ctx.row = r;
ctx.header_names = &hn_storage;
ctx.name_to_col = &name_to_col;
ctx.types_orig = eff_types.data();
ctx.n_types_orig = orig_cols;
ctx.derived = &stage0.derived;
ctx.derived_name_to_idx = &derived_n2i;
std::string e;
samples_str.emplace_back(
lua_engine::eval(lua_engine::get(), lid, ctx, &e));
}
} else {
// Recompute chain hasta stage ts output (sin aplicar derived
// del propio ts).
State st_sample = st;
// Limpia derived del target stage para que el sample no
// se referencie a si mismo.
if (ts < (int)st_sample.stages.size())
st_sample.stages[ts].derived.clear();
// Reusa la logica de materializacion: simple recompute manual.
// Aplica stage 0 (orig + derived) materializado.
State stmp = st;
Stage& s0 = stmp.stages[0];
for (auto& f : s0.filters) {
if (f.col >= 0 && f.col < eff_cols) f.col = src_for_eff[f.col];
}
s0.sorts.clear();
auto v0 = compute_visible_rows(cells, row_count, orig_cols, stmp);
std::vector<std::string> mb;
std::vector<const char*> mc;
mb.reserve((size_t)v0.size() * eff_cols);
mc.reserve((size_t)v0.size() * eff_cols);
for (int r : v0) {
for (int c = 0; c < eff_cols; ++c) {
if (c < orig_cols) {
const char* p = cells[r * orig_cols + c];
mb.emplace_back(p ? p : "");
} else {
const DerivedColumn& d = stage0.derived[c - orig_cols];
if (!d.formula.empty() && d.lua_id >= 0) {
lua_engine::RowCtx ctx;
ctx.cells = cells; ctx.orig_cols = orig_cols;
ctx.row = r;
ctx.header_names = &hn_storage;
ctx.name_to_col = &name_to_col;
ctx.types_orig = eff_types.data();
ctx.n_types_orig = orig_cols;
ctx.derived = &stage0.derived;
ctx.derived_name_to_idx = &derived_n2i;
std::string e;
mb.emplace_back(lua_engine::eval(lua_engine::get(), d.lua_id, ctx, &e));
} else if (d.source_col >= 0) {
const char* p = cells[r * orig_cols + d.source_col];
mb.emplace_back(p ? p : "");
} else mb.emplace_back("");
}
}
}
for (auto& s : mb) mc.push_back(s.c_str());
std::vector<std::string> ch(eff_cols);
std::vector<ColumnType> ct(eff_cols);
for (int c = 0; c < eff_cols; ++c) { ch[c] = eff_headers[c]; ct[c] = eff_types[c]; }
const char* const* cc = mc.data();
int cr = (int)v0.size();
int cn = eff_cols;
std::vector<StageOutput> tmps;
for (int si = 1; si <= ts; ++si) {
Stage stage_sn = st.stages[si];
// En el target stage NO apliques sus propias derived.
if (si == ts) stage_sn.derived.clear();
tmps.push_back(compute_stage(cc, cr, cn, ch, ct, stage_sn));
const StageOutput& l = tmps.back();
cc = l.cells.data(); cr = l.rows; cn = l.cols;
ch = l.headers; ct = l.types;
}
// Build name_to_col map for the target stage output.
std::vector<std::string> hn_t = ch;
std::unordered_map<std::string, int> n2c_t;
for (size_t i = 0; i < hn_t.size(); ++i) n2c_t[hn_t[i]] = (int)i;
sample = std::min(64, cr);
for (int r = 0; r < sample; ++r) {
lua_engine::RowCtx ctx;
ctx.cells = cc;
ctx.orig_cols = cn;
ctx.row = r;
ctx.header_names = &hn_t;
ctx.name_to_col = &n2c_t;
ctx.types_orig = ct.data();
ctx.n_types_orig = cn;
std::string e;
samples_str.emplace_back(
lua_engine::eval(lua_engine::get(), lid, ctx, &e));
}
}
std::vector<const char*> samples_ptr;
samples_ptr.reserve(samples_str.size());
for (auto& s : samples_str) samples_ptr.push_back(s.c_str());
ColumnType auto_t = auto_detect_type(samples_ptr.data(),
(int)samples_ptr.size(), 1, 0);
// Save to target stage.
if (ts < 0 || ts >= (int)st.stages.size()) ts = 0;
auto& target_derived = st.stages[ts].derived;
if (U.cf_editing && U.cf_edit_idx >= 0 &&
U.cf_edit_idx < (int)target_derived.size())
{
auto& d = target_derived[U.cf_edit_idx];
if (d.lua_id >= 0) lua_engine::release(lua_engine::get(), d.lua_id);
d.formula = U.cf_formula;
d.name = U.cf_name.empty() ? "custom" : U.cf_name;
d.type = auto_t;
d.lua_id = lid;
d.compile_error.clear();
} else {
DerivedColumn d;
d.source_col = -1;
d.type = auto_t;
d.name = U.cf_name.empty() ? "custom" : U.cf_name;
d.formula = U.cf_formula;
d.lua_id = lid;
target_derived.push_back(d);
}
U.cf_open = false;
U.cf_error.clear();
}
}
ImGui::SameLine();
if (ImGui::Button("Cancel")) {
U.cf_open = false;
U.cf_error.clear();
}
ImGui::EndPopup();
}
if (U.tql_show_open) ImGui::OpenPopup("Show TQL");
if (ImGui::BeginPopupModal("Show TQL", &U.tql_show_open,
ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::Text("TQL serializado del estado actual (read-only):");
ImGui::InputTextMultiline("##tqlshow", U.tql_show_text.data(),
U.tql_show_text.size() + 1,
ImVec2(560, 280),
ImGuiInputTextFlags_ReadOnly);
if (ImGui::Button("Copy to clipboard")) {
ImGui::SetClipboardText(U.tql_show_text.c_str());
}
ImGui::SameLine();
if (ImGui::Button("Close")) U.tql_show_open = false;
ImGui::EndPopup();
}
if (U.tql_apply_open) ImGui::OpenPopup("Apply TQL");
if (ImGui::BeginPopupModal("Apply TQL", &U.tql_apply_open,
ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::Text("Pega un chunk TQL (Lua). Ver docs/TQL.md para sintaxis.");
static char tql_buf[8192] = {0};
if (std::strcmp(tql_buf, U.tql_apply_text.c_str()) != 0) {
std::snprintf(tql_buf, sizeof(tql_buf), "%s", U.tql_apply_text.c_str());
}
if (ImGui::InputTextMultiline("##tqlapply", tql_buf, sizeof(tql_buf),
ImVec2(560, 280))) {
U.tql_apply_text = tql_buf;
}
if (!U.tql_apply_error.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(230, 100, 100, 255));
ImGui::TextWrapped("Error: %s", U.tql_apply_error.c_str());
ImGui::PopStyleColor();
}
if (ImGui::Button("Apply")) {
std::vector<std::string> orig_headers(orig_cols);
std::vector<ColumnType> orig_types(orig_cols);
for (int c = 0; c < orig_cols; ++c) {
orig_headers[c] = headers[c];
orig_types[c] = eff_types[c];
}
std::string err;
bool ok = tql::apply(U.tql_apply_text, st, orig_headers, orig_types,
cells, row_count, orig_cols, &err);
if (ok) {
U.tql_apply_open = false;
U.tql_apply_error.clear();
} else {
U.tql_apply_error = err;
}
}
ImGui::SameLine();
if (ImGui::Button("Cancel")) {
U.tql_apply_open = false;
U.tql_apply_error.clear();
}
ImGui::EndPopup();
}
// Ask AI modal (fase 11 — issue 0080).
if (U.ask_open) ImGui::OpenPopup("Ask AI");
ImGui::SetNextWindowSize(ImVec2(820, 560), ImGuiCond_Appearing);
if (ImGui::BeginPopupModal("Ask AI", &U.ask_open,
ImGuiWindowFlags_NoSavedSettings)) {
ImGui::TextDisabled("Ask en lenguaje natural. Default TQL. SQL solo si DuckDB linkado.");
const char* modes[] = {"TQL", "SQL (DuckDB)"};
#ifndef FN_TQL_DUCKDB
// SQL mode disabled visually pero el toggle existe (informativo)
if (U.ask_mode == 1) U.ask_mode = 0;
#endif
ImGui::Combo("Output##askmode", &U.ask_mode, modes, IM_ARRAYSIZE(modes));
#ifndef FN_TQL_DUCKDB
if (U.ask_mode == 1) {
ImGui::TextColored(ImVec4(1, 0.5f, 0.3f, 1),
"SQL mode requires FN_TQL_DUCKDB=1 build flag.");
}
#endif
ImGui::InputTextMultiline("##ask_q", U.ask_question, sizeof(U.ask_question),
ImVec2(-1, 80));
ImGui::BeginDisabled(U.ask_busy);
if (ImGui::Button("Send")) {
U.ask_busy = true;
U.ask_status = "Sending...";
U.ask_error.clear();
U.ask_response_code.clear();
U.ask_response_raw.clear();
// Build AskInput desde el state actual.
llm_anthropic::AskInput in;
in.question = U.ask_question;
in.tql_current = U.ask_current_tql;
in.col_names = U.active_headers;
in.col_types = U.active_types;
in.mode = (U.ask_mode == 1)
? llm_anthropic::OutputMode::SQL
: llm_anthropic::OutputMode::TQL;
// Llamada blocking (UI freeze breve durante red).
auto r = llm_anthropic::ask(in);
U.ask_busy = false;
if (!r.error.empty()) {
U.ask_error = r.error;
U.ask_status = "Error";
} else {
U.ask_response_raw = r.raw;
U.ask_response_code = r.code;
U.ask_status = "Got response.";
// Llenar edit buffer
std::snprintf(U.ask_edit_buf, sizeof(U.ask_edit_buf),
"%s", r.code.c_str());
}
}
ImGui::EndDisabled();
ImGui::SameLine();
if (!U.ask_status.empty()) {
ImGui::TextDisabled("%s", U.ask_status.c_str());
}
if (!U.ask_error.empty()) {
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", U.ask_error.c_str());
}
ImGui::Separator();
ImGui::Columns(2, "ask_cols", true);
ImGui::TextUnformatted("Current");
ImGui::InputTextMultiline("##ask_cur",
const_cast<char*>(U.ask_current_tql.c_str()),
U.ask_current_tql.size() + 1,
ImVec2(-1, 240),
ImGuiInputTextFlags_ReadOnly);
ImGui::NextColumn();
ImGui::TextUnformatted("Proposed (editable before apply)");
ImGui::InputTextMultiline("##ask_new", U.ask_edit_buf, sizeof(U.ask_edit_buf),
ImVec2(-1, 240));
ImGui::Columns(1);
bool can_apply = !U.ask_busy && U.ask_edit_buf[0] != '\0';
ImGui::BeginDisabled(!can_apply);
if (ImGui::Button("Apply")) {
std::string err;
if (U.ask_mode == 0) {
// TQL apply
bool ok = tql::apply(U.ask_edit_buf, st,
U.active_headers,
U.active_types,
nullptr, 0,
(int)U.active_headers.size(),
&err);
if (ok) {
U.ask_status = "Applied OK.";
U.ask_open = false;
} else {
U.ask_error = "tql::apply error: " + err;
U.ask_status = "Apply failed.";
}
} else {
#ifdef FN_TQL_DUCKDB
// SQL apply: ejecutar via tql_duckdb sobre TableInputs activas.
// Para tablas en memoria construimos un TableInput basico desde
// active_headers/types. v1 no recupera cells originales aqui;
// reportamos solo error si fallo. Caller real deberia pasar
// tables() del render scope. Sin esto, marcamos status info.
U.ask_status = "SQL execute disponible (FN_TQL_DUCKDB ON). "
"Integracion full pendiente: usar tql_duckdb::execute desde caller.";
#else
U.ask_status = "SQL execute requires FN_TQL_DUCKDB build flag.";
#endif
}
}
ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button("Reject")) {
U.ask_response_code.clear();
U.ask_edit_buf[0] = '\0';
}
ImGui::SameLine();
if (ImGui::Button("Close")) {
U.ask_open = false;
}
ImGui::EndPopup();
}
if (U.open_cell_popup) { ImGui::OpenPopup("##cell_op"); U.open_cell_popup = false; }
if (ImGui::BeginPopup("##cell_op")) {
ColumnType t = (U.pending_col >= 0 && U.pending_col < eff_cols)
? eff_types[U.pending_col] : ColumnType::String;
const char* hdr = (U.pending_col >= 0 && U.pending_col < eff_cols)
? eff_headers[U.pending_col] : "?";
ImGui::TextDisabled("%s %s ?? \"%s\"",
column_type_icon(t), hdr, U.pending_value.c_str());
ImGui::Separator();
auto ops = ops_for_type(t);
for (Op o : ops) {
if (ImGui::MenuItem(op_label(o))) {
st.stages[0].filters.push_back({U.pending_col, o, U.pending_value});
ImGui::CloseCurrentPopup();
}
}
ImGui::EndPopup();
}
}
} // namespace data_table