// 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 #include #include #include #include #include #include #include // 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 col_names; std::vector col_types; std::vector 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& stops, float t, float alpha) { // Default green→amber→red when caller provides no stops. static const std::vector 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 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 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* 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 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& 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 compute_visible_rows(const char* const* cells, int rows, int cols, const State& st) { std::vector 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 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& 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 build_preset_filters(FilterPreset preset, int col, const std::string& today_ymd) { std::vector 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 filter_inputs; std::unordered_map color_value_inputs; std::unordered_map 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 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 active_headers; std::vector 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 input_headers_active; std::vector 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 drill_back; std::vector 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& headers, const std::vector& types, const std::vector& 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& 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 next; next.reserve(eff_cols); for (int x : st.col_order) if (x >= 0 && x < eff_cols) next.push_back(x); std::vector 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 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* 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 all_names; std::vector num_names; std::vector 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& 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 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 collect_active_col_info(const State& st) { (void)st; auto& U = ui(); std::vector 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(), // emit headers stage 0 (caller fill si necesario) std::vector()); } 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& joinables, const std::vector& 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& 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& 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& 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& filters, const std::vector& visible, const std::vector& 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& 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& 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& 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& 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& 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& 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& tables, State& st, std::vector* 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 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 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* 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 joined_headers_store; static thread_local std::vector joined_types_store; static thread_local std::vector joined_headers_ptrs; static thread_local std::vector joined_cells_ptrs; static thread_local std::vector 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 cur_h(orig_cols); std::vector 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 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 eff_headers(eff_cols); std::vector src_for_eff(eff_cols); std::vector 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 hn_storage; static thread_local std::unordered_map name_to_col; static thread_local std::unordered_map 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{}(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 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 orig_headers(orig_cols); std::vector 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 oh(orig_cols); std::vector 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(f)), std::istreambuf_iterator()); std::vector oh(orig_cols); std::vector 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 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* 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 mat_backing; std::vector 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 cur_headers(eff_cols); std::vector 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 outs; outs.reserve(st.stages.size()); // Headers del INPUT del active (= output del active-1) std::vector input_headers_active = cur_headers; std::vector 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 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 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 ext_backing; std::vector ext_cells; std::vector ext_headers; std::vector ext_types; if (!act.derived.empty()) { int orig_out_cols = cur_cols_n; std::vector out_hn = cur_headers; std::unordered_map 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 oh(orig_cols); std::vector 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 oh(orig_cols); std::vector 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(f)), std::istreambuf_iterator()); std::vector oh(orig_cols); std::vector 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* 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. 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 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 mb; std::vector 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 ch(eff_cols); std::vector 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 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 hn_t = ch; std::unordered_map 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 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 orig_headers(orig_cols); std::vector 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(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