// data_table_grid — render del grid de datos con renderers declarativos. // Sub-funcion extraida de modules/data_table/data_table.cpp (issue 0107c). // // Contiene: // - Helpers estaticos de celda: hex_to_imcolor, lerp_color_along_stops, // icon_name_to_glyph, column_type_icon (copia local para este TU). // - draw_cell_custom: renderer declarativo por ColumnSpec. // - draw_row_inspector_modal: modal de inspeccion de fila. // - render_grid_stage0: grid completo para stage == 0. // - render_grid_stage_n: grid para stage > 0 (datos materializados). // // Dependencias: // viz/data_table_grid.h // viz/data_table_drill.h (make_drill_filter, apply_drill_step, drill_into) // viz/data_table_color_rules.h (parse_hex_color, apply_color_rules_for_cell) // viz/data_table_chips.h (apply_header_sort_click, draw_header_menu) // data_table/data_table_internal.h (UiState, ui(), reorder_column via data_table.cpp) // core/data_table_types.h // core/tql_helpers.h (parse_breakout_granularity) // core/auto_detect_type.h (parse_number) // imgui.h, core/icons_tabler.h #include "viz/data_table_grid.h" #include "viz/data_table_drill.h" #include "viz/data_table_color_rules.h" #include "viz/data_table_chips.h" #include "data_table/data_table_internal.h" #include "core/data_table_types.h" #include "core/tql_helpers.h" #include "core/auto_detect_type.h" #include "imgui.h" #include "core/icons_tabler.h" #include #include #include #include #include #include #include namespace data_table { // --------------------------------------------------------------------------- // Static helpers (only used by this TU) // --------------------------------------------------------------------------- static const char* column_type_icon_g(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 "?"; } static ImVec4 hex_to_imcolor_g(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); } static ImU32 lerp_color_along_stops_g( const std::vector& stops, float t, float alpha) { static const std::vector kDefault = { {0.0f, "#22c55e"}, {0.5f, "#f59e0b"}, {1.0f, "#ef4444"}, }; const auto& sv = stops.empty() ? kDefault : stops; std::vector sorted_sv = sv; std::sort(sorted_sv.begin(), sorted_sv.end(), [](const ColorStop& a, const ColorStop& b) { return a.position < b.position; }); t = t < 0.f ? 0.f : (t > 1.f ? 1.f : t); 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); 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_g(lo.color); ImVec4 cb = hex_to_imcolor_g(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 rv = ca.x + f * (cb.x - ca.x); float gv = ca.y + f * (cb.y - ca.y); float bv = ca.z + f * (cb.z - ca.z); unsigned int ri = (unsigned int)(rv * 255.f + 0.5f); unsigned int gi = (unsigned int)(gv * 255.f + 0.5f); unsigned int bi = (unsigned int)(bv * 255.f + 0.5f); unsigned int ai = (unsigned int)(alpha * 255.f + 0.5f); return IM_COL32(ri, gi, bi, ai); } } return parse_hex_color(sorted_sv.back().color, alpha); } static const char* icon_name_to_glyph_g(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; } // row_to_tsv_g: serializa una fila a header\tvalues. static std::string row_to_tsv_g(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; } // reorder_column_g: mueve col src a posicion de col dst en st.col_order. static void reorder_column_g(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); } // --------------------------------------------------------------------------- // draw_cell_custom // --------------------------------------------------------------------------- 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: { const BadgeRule* matched = nullptr; for (const auto& br : spec.badges) { if (br.value == value) { matched = &br; break; } } if (matched) { ImVec4 col = hex_to_imcolor_g(matched->color_hex); const char* label = matched->label.empty() ? value : matched->label.c_str(); if (col.x >= 0.f) { ImGui::PushStyleColor(ImGuiCol_Header, col); 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); 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_g(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_g("#22c55e"); } else if (ms <= spec.duration_error_ms) { text_col = hex_to_imcolor_g("#f59e0b"); } else { text_col = hex_to_imcolor_g("#ef4444"); } 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_g(entry.icon_name); if (!entry.color_hex.empty()) icon_col = hex_to_imcolor_g(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: { 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_g(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; } } 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: { 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(); 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); 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_g(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); } float total_w = (limit > 0 ? (limit * spacing) : 0.f); ImGui::Dummy(ImVec2(total_w, font_h)); 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: { 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); ImGui::Dummy(ImVec2(radius * 2.0f + 4.0f, font_h)); ImGui::SameLine(0, 0); } ImGui::TextUnformatted(value); break; } case CellRenderer::ColorScale: { 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); } t = t < 0.f ? 0.f : (t > 1.f ? 1.f : t); ImU32 bg_col = lerp_color_along_stops_g(spec.range_stops, t, spec.range_alpha); ImVec2 cell_min = ImGui::GetCursorScreenPos(); 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); ImGui::TextUnformatted(value); break; } default: ImGui::TextUnformatted(value); break; } // Tooltip (non-Dots renderers). 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); } } // --------------------------------------------------------------------------- // draw_row_inspector_modal (static — solo usada por render_grid_stage_n) // --------------------------------------------------------------------------- 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) { if (!st.inspect_open) return; if (st.inspect_row < 0 || st.inspect_row >= rows) { st.inspect_open = false; return; } ImGui::OpenPopup("##row_inspector"); ImGui::SetNextWindowSize(ImVec2(560, 400), ImGuiCond_Appearing); if (ImGui::BeginPopupModal("##row_inspector", &st.inspect_open, ImGuiWindowFlags_NoSavedSettings)) { ImGui::Text("Row %d", st.inspect_row); ImGui::SameLine(0, 20); if (ImGui::SmallButton("Copy TSV")) { std::string tsv = row_to_tsv_g(cells, rows, cols, st.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[st.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)) { st.drill_back.push_back(step); } } st.drill_forward.clear(); st.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_g(t), (c < (int)headers.size()) ? headers[c].c_str() : "?"); ImGui::TableSetColumnIndex(1); const char* v = cells[st.inspect_row * cols + c]; ImGui::TextWrapped("%s", v ? v : ""); } ImGui::EndTable(); } ImGui::EndPopup(); } } // --------------------------------------------------------------------------- // render_grid_stage0 // --------------------------------------------------------------------------- void render_grid_stage0(const char* id, State& st, const char* const* cells, int row_count, int orig_cols, int eff_cols, const char* const* eff_headers, const ColumnType* eff_types, const int* src_for_eff, const std::vector& visible_rows, const TableInput& main_t, std::vector* events_out) { UiState& U = ui(); Stage& act = st.stages[0]; // draw_header_menu takes const std::vector&; build a local view. std::vector eff_types_vec(eff_types, eff_types + eff_cols); int visible_cols = 0; for (int c = 0; c < eff_cols; ++c) if (st.col_visible[c]) ++visible_cols; if (visible_cols == 0) return; ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY; // Issue 0081-O.7: disable ALL Selectable bg painting; hover + selection // painted via TableSetBgColor below. 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); // Custom header row 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); ImGui::PushID(c); 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_g(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_g(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) { // Restore visible hover inside popup (outer table pushes to transparent). ImGui::PushStyleColor(ImGuiCol_HeaderHovered, IM_COL32( 70, 95, 140, 230)); ImGui::PushStyleColor(ImGuiCol_HeaderActive, IM_COL32( 55, 80, 120, 240)); ImGui::PushStyleColor(ImGuiCol_Header, IM_COL32( 0, 0, 0, 0)); draw_header_menu(st, act, c, eff_headers, eff_cols, eff_types_vec, orig_cols, true); ImGui::PopStyleColor(3); ImGui::EndPopup(); } // Stats overlay if (st.stats_mode && c < (int)st.stats_cache.size()) { const ColStats& s = st.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(st.sel_anchor_row, st.sel_end_row); int sel_rmax = std::max(st.sel_anchor_row, st.sel_end_row); int sel_cmin = std::min(st.sel_anchor_col, st.sel_end_col); int sel_cmax = std::max(st.sel_anchor_col, st.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++); // Capture cell rect before render (for per-cell hover overlay). 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]; // NOTE: Lua-derived cell evaluation is intentionally NOT done here. // The caller (data_table.cpp entrypoint) evaluates derived cells and // passes a pre-materialized `cells` array to render_grid_stage0, OR // this function operates on the raw cells with src mapping. // For stage 0, derived cols are evaluated inline in the entrypoint // and NOT passed through this function path — the entrypoint calls // render_grid_stage0 only for the raw-cell + src_for_eff path, while // derived evaluation stays in the entrypoint context. // This avoids passing Lua state through the grid API boundary. const char* cell = cells[r * orig_cols + src]; float dot_advance = 0.f; apply_color_rules_for_cell(st, c, cell, cell_min, cell_w, cell_h, dot_advance); if (dot_advance > 0.f) { ImGui::SetCursorPosX(ImGui::GetCursorPosX() + dot_advance); } bool in_sel = (st.sel_active && ri >= sel_rmin && ri <= sel_rmax && oc >= sel_cmin && oc <= sel_cmax); ImGui::PushID(r * eff_cols + c); { 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) { ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0, 0, 0, 0)); ImGui::Selectable(cell ? cell : "", in_sel, ImGuiSelectableFlags_AllowDoubleClick, ImVec2(0, ImGui::GetTextLineHeight())); ImGui::PopStyleColor(); 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); } } } // Per-cell bg via TableSetBgColor (edge-to-edge, 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) { 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)); } } bool hovered = ImGui::IsItemHovered( ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); if (hovered) { if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { st.sel_anchor_row = ri; st.sel_anchor_col = oc; st.sel_end_row = ri; st.sel_end_col = oc; st.sel_active = true; st.sel_dragging = true; } else if (st.sel_dragging) { st.sel_end_row = ri; st.sel_end_col = oc; } 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)); } 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 (st.sel_dragging && !ImGui::IsMouseDown(ImGuiMouseButton_Left)) { st.sel_dragging = false; } ImGui::EndTable(); } ImGui::PopStyleColor(3); // HeaderHovered/HeaderActive/Header // Ctrl+C -> TSV copy if (st.sel_active && ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_C, false)) { int rmin = std::min(st.sel_anchor_row, st.sel_end_row); int rmax = std::max(st.sel_anchor_row, st.sel_end_row); int cmin = std::min(st.sel_anchor_col, st.sel_end_col); int cmax = std::max(st.sel_anchor_col, st.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_grid_stage_n // --------------------------------------------------------------------------- void render_grid_stage_n(const char* id, State& st, const char* const* cur_cells, int cur_rows, int cur_cols_n, const std::vector& cur_headers, const std::vector& cur_types, const std::vector& input_headers_active, int n_breakouts, const TableInput& main_t, std::vector* events_out) { UiState& U = ui(); int active = st.active_stage; Stage& act = st.stages[active]; ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY; // Issue 0081-O.7: tone down row hover. 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: icon + name + sort indicator + stats overlay. ImGui::TableNextRow(ImGuiTableRowFlags_Headers); for (int c = 0; c < cur_cols_n; ++c) { ImGui::TableSetColumnIndex(c); 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_g(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 (st.stats_mode && c < (int)st.stats_cache.size()) { const ColStats& s = st.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(); } } 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); { 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) { ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0, 0, 0, 0)); ImGui::Selectable(cell ? cell : "", false, 0, ImVec2(0, ImGui::GetTextLineHeight())); ImGui::PopStyleColor(); 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 : ""; st.inspect_row = r; ImGui::OpenPopup("##drill_popup"); 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_breakouts) { 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...")) { st.inspect_row = r; st.inspect_open = true; ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); } ImGui::PopID(); } } ImGui::EndTable(); } ImGui::PopStyleColor(3); // HeaderHovered/HeaderActive/Header // Row inspector modal draw_row_inspector_modal(st, active, cur_cells, cur_rows, cur_cols_n, cur_headers, cur_types, input_headers_active); } } // namespace data_table