// data_table_chips — barra de chips superior de la tabla TQL. // Sub-funcion extraida de modules/data_table/data_table.cpp (issue 0107c). // // Contiene: draw_joins_chips, draw_filter_chips, draw_breakout_chips, // draw_aggregation_chips, draw_sort_chips, apply_header_sort_click, // draw_edit_*_popup, draw_add_*_popup, draw_header_menu, draw_tql_bar. #include "viz/data_table_chips.h" #include "data_table/data_table_internal.h" // UiState, ui(), helpers inline #include "core/tql_emit.h" #include "core/tql_apply.h" #include "core/tql_helpers.h" // parse_breakout_granularity, date_granularity_token #include "viz/data_table_color_rules.h" // draw_color_rule_menu #include "imgui.h" #include "app_base.h" // fn::local_path #include #include #include #include #include #include #include namespace data_table { // --------------------------------------------------------------------------- // Static helpers (solo usados en este TU) // --------------------------------------------------------------------------- // column_type_icon: returns a Tabler icon UTF-8 sequence for each ColumnType. // (duplicado de data_table.cpp — lo dejamos static aqui para no depender del // entrypoint; si se necesita compartir mover a data_table_internal.h inline.) static const char* column_type_icon_chips(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 "?"; } // agg_fn_label_chips: nombre del fn de agregacion. static const char* agg_fn_label_chips(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 "?"; } // date helpers (solo para draw_add_breakout_popup) namespace { static bool parse_ymd_chips(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_chips(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 DateGranularity auto_date_gran_chips(const std::string& min_ymd, const std::string& max_ymd) { int y1,m1,d1, y2,m2,d2; if (!parse_ymd_chips(min_ymd, y1,m1,d1)) return DateGranularity::Day; if (!parse_ymd_chips(max_ymd, y2,m2,d2)) return DateGranularity::Day; long span = ymd_to_days_chips(y2,m2,d2) - ymd_to_days_chips(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 void column_min_max_chips(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; } } } } // anon // compose_breakout helper (local) static std::string compose_breakout_chips(const std::string& col, DateGranularity g) { if (g == DateGranularity::None) return col; return col + ":" + date_granularity_token(g); } // --------------------------------------------------------------------------- // draw_typed_ops // --------------------------------------------------------------------------- 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; } // --------------------------------------------------------------------------- // type_supports_range // --------------------------------------------------------------------------- bool type_supports_range(ColumnType t) { return t == ColumnType::Int || t == ColumnType::Float || t == ColumnType::Date; } // --------------------------------------------------------------------------- // apply_header_sort_click // --------------------------------------------------------------------------- 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); } } // --------------------------------------------------------------------------- // draw_sort_chips // --------------------------------------------------------------------------- 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); 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_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); } } // --------------------------------------------------------------------------- // draw_add_sort_popup // --------------------------------------------------------------------------- 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_chips(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(); } // --------------------------------------------------------------------------- // draw_edit_sort_popup // --------------------------------------------------------------------------- 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(); } // --------------------------------------------------------------------------- // draw_filter_chips // --------------------------------------------------------------------------- void draw_filter_chips(Stage& stg, const char* const* eff_headers, int eff_cols, const std::vector& eff_types) { auto& U = ui(); // Preset filter helpers needed locally auto today_iso_chips = []() -> std::string { 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; }; 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 menu 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; } // build_preset_filters lambda auto build_pf = [&](FilterPreset preset, int col) -> std::vector { std::vector out; std::string today = today_iso_chips(); auto last_n = [&](int n) { int y, m, d; if (!parse_ymd_chips(today, y, m, d)) return; long days = ymd_to_days_chips(y, m, d) - n; // reverse 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 doy2 = doe - (365*yoe + yoe/4 - yoe/100); unsigned mp = (5*doy2 + 2)/153; unsigned day = doy2 - (153*mp + 2)/5 + 1; unsigned mon = mp < 10 ? mp + 3 : mp - 9; if (mon <= 2) yr += 1; char buf[16]; std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d", yr, (int)mon, (int)day); 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; }; auto apply_preset = [&](FilterPreset p, int col) { auto fs = build_pf(p, col); 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); 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(); } // --------------------------------------------------------------------------- // draw_add_filter_popup // --------------------------------------------------------------------------- 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_chips(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_chips(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(); } // --------------------------------------------------------------------------- // draw_edit_filter_popup // --------------------------------------------------------------------------- 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(); } // --------------------------------------------------------------------------- // draw_breakout_chips // --------------------------------------------------------------------------- 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); 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; } 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_chips(col_name, o); } } ImGui::EndCombo(); } } ImGui::SameLine(); ++i; } ImGui::NewLine(); } // --------------------------------------------------------------------------- // draw_add_breakout_popup // --------------------------------------------------------------------------- void draw_add_breakout_popup(Stage& stg, const char* const* in_headers, int in_cols, const std::vector& in_types, const char* const* cur_cells, int cur_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_chips(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]; if (c >= 0 && c < (int)in_types.size() && in_types[c] == ColumnType::Date) { std::string lo, hi; column_min_max_chips(cur_cells, cur_rows, in_cols, c, lo, hi); DateGranularity g = auto_date_gran_chips(lo, hi); stg.breakouts.emplace_back(compose_breakout_chips(col, g)); } else { stg.breakouts.emplace_back(col); } ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); } // --------------------------------------------------------------------------- // draw_edit_breakout_popup // --------------------------------------------------------------------------- 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(); } // --------------------------------------------------------------------------- // draw_aggregation_chips // --------------------------------------------------------------------------- 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_chips(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(); } // --------------------------------------------------------------------------- // draw_add_aggregation_popup // --------------------------------------------------------------------------- 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_chips(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_chips(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_chips(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(); } // --------------------------------------------------------------------------- // draw_edit_agg_popup // --------------------------------------------------------------------------- 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_chips((AggFn)U.edit_agg_fn))) { for (auto f : all_fns) { bool sel = ((int)f == U.edit_agg_fn); if (ImGui::Selectable(agg_fn_label_chips(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(); } // --------------------------------------------------------------------------- // draw_joins_chips // Firma del header: (State&, const std::vector&, const char* const*, int, const std::vector&) // --------------------------------------------------------------------------- void draw_joins_chips(State& st, const std::vector& joinables, const char* const* eff_headers, int eff_cols, const std::vector& /*eff_types*/) { 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-join. State variables are static (one join popup at a time). 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"); if (pick_source_idx >= (int)joinables.size()) pick_source_idx = 0; 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 eff_headers) ImGui::SetNextItemWidth(180); const char* lcur = (pick_left_col >= 0 && pick_left_col < eff_cols) ? eff_headers[pick_left_col] : "?"; if (ImGui::BeginCombo("left col", lcur)) { for (int k = 0; k < eff_cols; ++k) { bool sel = (k == pick_left_col); if (ImGui::Selectable(eff_headers[k], 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") && pick_left_col < eff_cols && pick_right_col < (int)src.headers.size()) { Join jn; jn.alias = pick_alias; jn.source = src.name; jn.on.push_back({std::string(eff_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(); } // --------------------------------------------------------------------------- // draw_header_menu // --------------------------------------------------------------------------- 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(); } 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_chips(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")) { draw_color_rule_menu(st, col, t, U.color_rules); 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_chips(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(); } } // --------------------------------------------------------------------------- // draw_tql_bar // Extrae el bloque TQL de render() (Show/Apply/Save/Load + modales). // En el entrypoint data_table.cpp, reemplazar el bloque inline por llamadas // a draw_tql_bar(U.tql_bar, st, orig_headers, orig_types, cells, row_count, orig_cols). // --------------------------------------------------------------------------- void draw_tql_bar(TqlBarState& tql_bar, State& st, const std::vector& orig_headers, const std::vector& orig_types, const char* const* cells, int row_count, int orig_cols) { if (ImGui::SmallButton("Show TQL")) { tql_bar.show_text = tql::emit(st, orig_headers, orig_types); tql_bar.show_open = true; } ImGui::SameLine(); if (ImGui::SmallButton("Apply TQL")) { tql_bar.apply_open = true; tql_bar.apply_error.clear(); } ImGui::SameLine(); ImGui::SetNextItemWidth(160); ImGui::InputText("##tql_file", tql_bar.file_path, sizeof(tql_bar.file_path)); ImGui::SameLine(); if (ImGui::SmallButton("Save .tql")) { std::string text = tql::emit(st, orig_headers, orig_types); const char* path = fn::local_path(tql_bar.file_path); std::ofstream f(path); if (f) { f << text; tql_bar.io_status = std::string("saved: ") + path; } else { tql_bar.io_status = std::string("save FAILED: ") + path; } } ImGui::SameLine(); if (ImGui::SmallButton("Load .tql")) { const char* path = fn::local_path(tql_bar.file_path); std::ifstream f(path); if (!f) { tql_bar.io_status = std::string("load FAILED: ") + path; } else { std::string text((std::istreambuf_iterator(f)), std::istreambuf_iterator()); std::string err; bool ok = tql::apply(text, st, orig_headers, orig_types, cells, row_count, orig_cols, &err); if (ok) tql_bar.io_status = std::string("loaded: ") + path + (err.empty() ? "" : " (warn: " + err + ")"); else tql_bar.io_status = std::string("load parse error: ") + err; } } if (!tql_bar.io_status.empty()) { ImGui::SameLine(); ImGui::TextDisabled("%s", tql_bar.io_status.c_str()); } // Modal "Show TQL" if (tql_bar.show_open) { ImGui::OpenPopup("##tql_show_modal"); tql_bar.show_open = false; } ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_Appearing); if (ImGui::BeginPopupModal("##tql_show_modal", nullptr, ImGuiWindowFlags_NoSavedSettings)) { ImGui::TextUnformatted("TQL (read-only):"); ImGui::SameLine(); if (ImGui::SmallButton("Copy")) { ImGui::SetClipboardText(tql_bar.show_text.c_str()); } ImGui::InputTextMultiline("##tql_show_text", const_cast(tql_bar.show_text.c_str()), tql_bar.show_text.size() + 1, ImVec2(-1, -40), ImGuiInputTextFlags_ReadOnly); if (ImGui::Button("Close")) ImGui::CloseCurrentPopup(); ImGui::EndPopup(); } // Modal "Apply TQL" if (tql_bar.apply_open) { ImGui::OpenPopup("##tql_apply_modal"); tql_bar.apply_open = false; // Pre-fill apply_text with current TQL tql_bar.apply_text = tql::emit(st, orig_headers, orig_types); } ImGui::SetNextWindowSize(ImVec2(600, 420), ImGuiCond_Appearing); if (ImGui::BeginPopupModal("##tql_apply_modal", nullptr, ImGuiWindowFlags_NoSavedSettings)) { ImGui::TextUnformatted("Paste / edit TQL and click Apply:"); tql_bar.apply_text.resize(4096, '\0'); ImGui::InputTextMultiline("##tql_apply_text", tql_bar.apply_text.data(), tql_bar.apply_text.size(), ImVec2(-1, -80), ImGuiInputTextFlags_None); if (!tql_bar.apply_error.empty()) { ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 100, 100, 255)); ImGui::TextWrapped("Error: %s", tql_bar.apply_error.c_str()); ImGui::PopStyleColor(); } if (ImGui::Button("Apply")) { std::string err; bool ok = tql::apply(std::string(tql_bar.apply_text.c_str()), st, orig_headers, orig_types, cells, row_count, orig_cols, &err); if (ok) { tql_bar.apply_error.clear(); ImGui::CloseCurrentPopup(); } else { tql_bar.apply_error = err; } } ImGui::SameLine(); if (ImGui::Button("Cancel")) { tql_bar.apply_error.clear(); ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); } } } // namespace data_table