---
name: data_table
kind: function
lang: cpp
domain: viz
version: "1.5.0"
purity: impure
signature: "void data_table::render(const char* id, const std::vector<TableInput>& tables, State& st, std::vector<TableEvent>* events_out = nullptr, bool show_chrome = true)"
description: "Render UI completa de tabla TQL: chips bar, tabla, viz panels, column-stats inline, drill, color rules, joins, TQL editor, Ask AI, Button renderer, event sink (ButtonClick/RowDoubleClick/RowRightClick), tooltip per-cell, column_specs persisted in TQL. Dots renderer para sparkline-like de status (v1.3.0). CategoricalChip + ColorScale en v1.4.0. v1.5.0: stats/seleccion/drill-history/row-inspector pasan a vivir en State (por tabla, antes eran globales — multiples tablas en pantalla compartian estado). Conditional color extendido con tres modos: CellBg (legacy), CategoricalDot (autopalette por valor) y NumericRange (gradiente N-color). Hover del menu contextual de header restaurado. Entry-point publica del stack data_table. Muta State segun interaccion del usuario."
tags: [tables, viz, ui, imgui, tql, cpp-tables]
uses_functions:
  - compute_stage_cpp_core
  - compute_pipeline_cpp_core
  - compute_column_stats_cpp_core
  - auto_detect_type_cpp_core
  - tql_emit_cpp_core
  - tql_apply_cpp_core
  - tql_helpers_cpp_core
  - tql_to_sql_cpp_core
  - lua_engine_cpp_core
  - join_tables_cpp_core
  - viz_render_cpp_viz
uses_types:
  - data_table_types_cpp_core
  - ColumnSpec_cpp_core
  - CellRenderer_cpp_core
  - BadgeRule_cpp_core
  - IconMapEntry_cpp_core
  - TableInput_cpp_core
  - State_cpp_core
  - Stage_cpp_core
  - StageOutput_cpp_core
  - ViewMode_cpp_viz
  - ViewConfig_cpp_viz
  - VizPanel_cpp_viz
  - Join_cpp_core
  - Filter_cpp_core
  - DrillStep_cpp_core
  - DerivedColumn_cpp_core
  - Aggregation_cpp_core
  - SortClause_cpp_core
  - ColumnType_cpp_core
  - ColorRule_cpp_core
returns: []
returns_optional: false
error_type: "error_go_core"
imports:
  - imgui.h
  - app_base.h
  - core/data_table_types.h
  - core/lua_engine.h
  - core/tql_apply.h
  - core/tql_emit.h
  - core/tql_helpers.h
  - core/tql_to_sql.h
  - core/compute_stage.h
  - core/compute_pipeline.h
  - core/compute_column_stats.h
  - core/auto_detect_type.h
  - core/join_tables.h
  - viz/viz_render.h
tested: true
tests:
  - "back-compat: TableInput without column_specs does not crash"
  - "Badge: TableInput with Badge column_spec compiles and links"
  - "Progress: TableInput with Progress column_spec compiles and links"
  - "Duration: TableInput with Duration column_spec compiles and links"
  - "Icon: TableInput with Icon column_spec compiles and links"
  - "Button: TableEvent struct constructible; render() with events_out links"
  - "Tooltip: ColumnSpec with tooltip_on_hover=true compiles and links"
  - "Back-compat: both render() signatures (with/without events_out) link"
  - "Dots: ColumnSpec with CellRenderer::Dots + badges constructs correctly"
  - "Dots TQL roundtrip: State::aux_column_specs accepts Dots spec"
  - "TestCategoricalChipRule: chip rule with match='success' produces correct color"
  - "TestColorScaleLerpTwoStops: t=0→first color, t=1→last color, t=0.5→midpoint"
  - "TestColorScaleLerpThreeStops: t=0.25 lies between stop0 and stop1"
  - "TestColorScaleOutOfRange: t<0 saturates at first; t>1 saturates at last"
test_file_path: "cpp/tests/test_column_specs.cpp"
file_path: "modules/data_table/data_table.cpp"
params:
  - name: id
    desc: "ID unico ImGui para esta instancia, ej. '##orders_table'. Debe ser estable entre frames."
  - name: tables
    desc: "Lista de TableInput materializadas en memoria. tables[0] es la main por defecto; si State.main_source no-vacio se usa por nombre. Tablas extra se exponen como joinables en la UI de joins."
  - name: st
    desc: "Estado mutable completo: pipeline de stages, joins, viz config, ui tweaks, aux_column_specs (Phase 2). Debe persistir entre frames — no declarar en el stack del frame."
  - name: events_out
    desc: "Puntero a vector de TableEvent. Si non-null, se puebla con eventos de este frame (ButtonClick, RowDoubleClick, RowRightClick). El caller limpia/lee el vector despues de cada render. Pasar nullptr para desactivar (back-compat)."
  - name: show_chrome
    desc: "Si false, oculta chips bar + breadcrumb por defecto. El usuario puede reactivar con el boton 'Show UI'. El State persiste el override del usuario entre frames."
output: "void. Muta st en respuesta a la interaccion del usuario (filtros, breakouts, sorts, drill, joins, viz mode). Los cambios son visibles en st al retornar. Events emitted via events_out."
---

## Ejemplo

```cpp
#include "viz/data_table.h"
#include "core/data_table_types.h"

// --- Setup (una vez) ---
data_table::TableInput t;
t.name = "orders";
t.rows = num_rows;
t.cols = num_cols;
t.cells = cells_ptr;          // row-major flat array, owner externo
t.headers = {"id", "amount", "status", "actions"};
t.types   = {data_table::ColumnType::Int,
             data_table::ColumnType::Float,
             data_table::ColumnType::String,
             data_table::ColumnType::String};

// Phase 2: declarative renderers + tooltip
t.column_specs.resize(4);
t.column_specs[2].renderer = data_table::CellRenderer::Badge;
t.column_specs[2].badges   = {{"paid","#22c55e","Paid"},{"pending","#f59e0b",""}};
t.column_specs[2].tooltip         = "auto";
t.column_specs[2].tooltip_on_hover = true;
t.column_specs[3].renderer     = data_table::CellRenderer::Button;
t.column_specs[3].button_action = "cancel_order";
t.column_specs[3].button_label  = "Cancel";

data_table::State st;          // persiste entre frames
std::vector<data_table::TableEvent> events;

// --- Render (cada frame) ---
ImGui::Begin("Orders");
ImGui::BeginChild("##tbl", ImVec2(-1, -1));
events.clear();
data_table::render("##orders", {t}, st, &events);
ImGui::EndChild();
ImGui::End();

// --- Process events ---
for (const auto& ev : events) {
    if (ev.kind == data_table::TableEventKind::ButtonClick &&
        ev.action_id == "cancel_order") {
        cancel_order(ev.row);   // app handles the action
    }
    if (ev.kind == data_table::TableEventKind::RowDoubleClick) {
        open_order_detail(ev.row);
    }
}
```

## Cuando usarla

Cuando una app necesita tabla con filtros + agregaciones + viz + joins sobre datos en memoria. Reemplaza `ImGui::BeginTable` inline + toda la logica TQL manual. Sustituye directamente el include del playground (`tables/data_table.h`) cambiando solo el path a `viz/data_table.h`.

Usar `CategoricalChip` cuando quieras un indicador visual (dot de color) siempre visible a la izquierda del texto de la celda, para columnas categoricas (estado, tipo, severidad). Mas discreto que Badge y sin hover-only. Usar `ColorScale` cuando la columna sea numerica continua y quieras dar contexto visual de "alto/bajo/medio" con un fondo tintado proporcional al valor — util para latencias, scores, porcentajes, metricas.

## Per-table state (v1.5.0)

Antes de v1.5.0 los siguientes campos vivian en un singleton `UiState` interno y se aplicaban a TODAS las tablas a la vez. Desde v1.5.0 viven en `State` (parametro `st`), por tabla:

| Campo | Significado |
|---|---|
| `stats_mode`, `stats_cache`, `stats_last_*` | Toggle "Show stats" + cache de stats por col. |
| `sel_anchor_row/col`, `sel_end_row/col`, `sel_active`, `sel_dragging` | Seleccion rectangular drag para Ctrl+C (TSV). |
| `inspect_row`, `inspect_open` | Modal "Inspect row..." del menu contextual. |
| `drill_back`, `drill_forward` | Historial de drill back/forward. |

Estado modal (un solo popup abierto a la vez en toda la app) sigue en el singleton interno: edit-chip popups, addfilter/addbreakout/addagg/addsort, Ask AI, Custom column formula, TQL show/apply, drill popup, header context. Estos NO necesitan separacion por tabla porque ImGui solo permite un popup abierto a la vez.

## Conditional color rules (v1.5.0)

`State::color_rules` (tipo `std::vector<ColorRule>`) acepta tres modos via `ColorRule::kind` (enum `ColorRuleKind`):

| Modo | Cuando usar | Campos relevantes |
|---|---|---|
| `CellBg` (legacy) | Resaltar bg de celda completa cuando valor == `equals`. Un color por regla, multiples reglas por col. | `equals`, `color` |
| `CategoricalDot` | Categoricas con muchos valores distintos. Dibuja un dot circular a la izquierda del texto. Color autoasignado por hash (palette de 12 vibrant Tailwind-500) o por mapeo explicito `dot_map`. | `dot_alpha`, `dot_radius_px`, `dot_map` |
| `NumericRange` | Numericas continuas. Gradiente N-color sobre bg de celda. Valores fuera de `[range_min, range_max]` saturan en los stops extremos. | `range_min`, `range_max`, `range_alpha`, `range_stops` |

### Menu UI

`Right-click cabecera → Conditional color → [Cell bg / Categorical dot / Numeric range]`. La auto-deteccion preselecciona NumericRange para columnas Int/Float, CellBg para el resto. Cada Apply REEMPLAZA la regla existente del mismo `kind` en esa columna (puedes combinar las tres en la misma columna). "Clear col" borra todas las reglas de la columna.

### Ejemplo programatico

```cpp
data_table::State st;

// Cell bg cuando status == "error" -> rojo.
data_table::ColorRule r1;
r1.col    = 3;
r1.kind   = data_table::ColorRuleKind::CellBg;
r1.equals = "error";
r1.color  = IM_COL32(220, 60, 60, 120);
st.color_rules.push_back(r1);

// Dot autoasignado por categoria de `region`.
data_table::ColorRule r2;
r2.col           = 1;
r2.kind          = data_table::ColorRuleKind::CategoricalDot;
r2.dot_alpha     = 1.0f;
r2.dot_radius_px = 4.0f;
// (dot_map vacio -> autopalette por hash. Para mapeo fijo:)
// r2.dot_map = {{"EU","#3b82f6"},{"NA","#22c55e"},{"AS","#f59e0b"}};
st.color_rules.push_back(r2);

// Gradient verde→ambar→rojo sobre latency_ms en [0, 500].
data_table::ColorRule r3;
r3.col          = 5;
r3.kind         = data_table::ColorRuleKind::NumericRange;
r3.range_min    = 0.0;
r3.range_max    = 500.0;
r3.range_alpha  = 0.25f;
r3.range_stops  = {
    {0.0f, "#22c55e"},
    {0.5f, "#f59e0b"},
    {1.0f, "#ef4444"},
};
st.color_rules.push_back(r3);
```

### CategoricalDot vs CategoricalChip renderer

Hay dos rutas para dots categoricos. Cuando elegir cada una:

| Mecanismo | Donde se define | Cuando |
|---|---|---|
| `ColorRule::CategoricalDot` (v1.5.0) | `State::color_rules` (ad-hoc por usuario via menu) | Exploracion interactiva: el usuario decide en runtime si pintar dots y con que palette. Autopalette por hash sin definir mapeo. |
| `CellRenderer::CategoricalChip` (v1.4.0) | `TableInput::column_specs[c]` (declarativo por el caller) | App quiere dots SIEMPRE para una columna especifica con mapeo explicito `chips: [{match, color}]`. |

Ambos son compatibles — pueden coexistir sin colision.

## Gotchas

- **ImGui + ImPlot context activos**: `render()` llama a APIs de ambas librerias. Llamar fuera de un frame activo causa UB.
- **State no stack-local**: `State` contiene el historial de drill, pipeline de stages, cache de stats y buffers de UI. Declarar en el stack del frame reset todo el estado del usuario en cada frame.
- **Drill-down propaga en State**: `st.active_stage` y `st.stages` se mutan por click en charts. El caller puede leer `st` tras `render()` para reaccionar.
- **Thread-safety**: `render()` usa `static thread_local` para buffers intermedios. Llamar solo desde el main thread de ImGui.
- **TableInput owner externo**: `cells` es un puntero raw al array del caller. Los datos deben sobrevivir durante toda la llamada a `render()`. No pasar puntero a vector que puede reallocarse.
- **events_out no se limpia**: `render()` solo hace `push_back`. El caller debe llamar `events.clear()` antes de cada frame o acumulara eventos de frames anteriores.
- **Button + celda vacia**: si el cell value es vacio, el boton NO se dibuja. La app controla cuando mostrar el boton poniendo un value no vacio (ej. "1" o el ID de la fila).
- **RowRightClick emite evento Y abre popup interno**: la tabla de stages (stage>0) sigue abriendo su popup de drill. En el raw table (stage 0), se emite el evento pero el popup de drill antiguo tambien puede abrirse via `U.open_cell_popup`. El caller puede ignorar el popup interno y gestionar su propio menu al detectar `RowRightClick`.
- **CategoricalChip sin regla coincidente → sin dot**: si ninguna `ChipRule.match` coincide con el valor de la celda, solo se renderiza el texto. Definir una regla de fallback explicita si se necesita dot para valores no mapeados.
- **ColorScale clampa fuera de rango**: valores por debajo de `range_min` se tratan como t=0 (primer stop) y valores por encima de `range_max` como t=1 (ultimo stop). Definir `range_min`/`range_max` sensatos para que el gradiente sea informativo; valores muy alejados de la mayoria hacen que todo el gradiente aparezca en un extremo.
- **aux_column_specs merge**: si `TableInput.column_specs` esta vacio pero `State.aux_column_specs[0]` no, `render()` los aplica automaticamente. Si el caller pasa column_specs no vacios, ganan sobre los del State.
- **Ask AI modal (llm_anthropic)**: el boton "Ask AI" usa un stub interno de `llm_anthropic` que retorna error por defecto. Para activar la feature real, compilar con `-DFN_LLM_ANTHROPIC=1` y proveer `infra/llm_anthropic.h` en el include path. Pendiente Wave 4: promover al registry.
- **FN_TQL_DUCKDB**: modo SQL del Ask AI requiere compilar con `-DFN_TQL_DUCKDB=1` y la libreria DuckDB disponible.

## Notas

No hay tests unitarios directos: `render()` requiere ImGui + ImPlot context activos (imposible sin ventana GL). Cobertura via:
1. `cpp/apps/primitives_gallery/playground/tables/` — playground original con self_test.cpp y e2e_run.sh.
2. Wave 4: migration self-tests en las apps que migren desde el playground.

**Estado Wave 3.5 (issue 0081-I):**
- Todos los includes del playground (`data_table_logic.h`, `tql.h`, `tql_to_sql.h`) eliminados. `data_table.cpp` compila sin el playground en el include path.
- `tql::apply` firma extendida ya en `tql_apply_cpp_core` (wave anterior). Resuelto.
- `tql_to_sql` promovido a `core/tql_to_sql.h`. Resuelto.
- `data_table_logic` helpers (row_to_tsv, drill, view_mode, etc.) declarados como `static` en `data_table.cpp`. No son API pública.
- `State::ensure_stage0/raw/active` implementados en `compute_stage.cpp`.
- `ColStats` struct: usa el de `compute_column_stats_cpp_core`. Unificado.

**Deuda tecnica restante (Wave 4):**
- `llm_anthropic` (Ask AI modal, issue 0080): stub interno activo. Promover a `cpp/functions/infra/llm_anthropic` para activar feature real.
- `FN_TQL_DUCKDB`: modo SQL del Ask AI sin soporte en stub. Requiere DuckDB + flag de compilacion.
- `column_specs` TQL roundtrip (Phase 2): actualmente caller-managed. No persisten en TQL emit/apply. Planificado en issue 0081-O.

## Capability growth log

<!-- ANADIR CUANDO PASE EL GATE 0133:
v1.0.0-stable (YYYY-MM-DD) — finalize: columnar snapshot + string interning + lazy filter/sort + display cache + OpenMP compute. Bench 10M rows >=60fps. API publica frozen: render(), TableInput, State, TableEvent, ColumnSpec, ColorRule no admiten breaking changes sin major bump. Ver MIGRATION.md para contratos exactos.
-->

v1.1.0 (2026-05-15) — declarative CellRenderer (Badge/Progress/Duration/Icon) via TableInput.column_specs sidecar. Back-compat preservado: apps existentes sin column_specs siguen funcionando sin cambios.

v1.2.0 (2026-05-15) — Button renderer + event sink (ButtonClick/RowDoubleClick/RowRightClick) + tooltip per cell + column_specs persisted in TQL (aux_column_specs roundtrip). Back-compat preserved: events_out=nullptr by default; existing render() callers unchanged.

v1.3.0 (2026-05-15) — Dots renderer for inline status timelines (sparkline-like). Reuses badges for color mapping. dots_max/dots_separator/dots_show_count/dots_glyph_size fields. TQL roundtrip. dag_engine_ui canonical use case (10-col antipattern -> 6-col fix).

v1.3.1 (2026-05-15) — Dots renderer now draws filled circles via ImDrawList instead of Unicode glyph. Font-independent: works regardless of TTF glyph coverage. Closes "dots show as ?" bug in dag_engine_ui.

v1.3.2 (2026-05-15) — Hover dimming: row uses muted alpha (0.05 vs default 0.31); hovered cell gets a subtle overlay (~9% white) via ImDrawList. Badge no longer SpanAllColumns. Closes "table-wide bright highlight on hover".

v1.3.3 (2026-05-15) — Selectable bg disabled for Text/empty cells (was duplicating with manual overlay → gray double-hover). Explicit ImVec2 size on Selectable so empty cells get a hit area (fixes drag-select skipping empties). Single uniform hover layer across all cell renderers.

v1.3.4 (2026-05-15) — Row height tightened: GetTextLineHeight() (no spacing) replaces GetTextLineHeightWithSpacing() in Selectable size + manual overlay rect. Removes inflated row vertical padding introduced in v1.3.3.

v1.3.5 (2026-05-15) — Cell hover paints via TableSetBgColor (covers entire cell bg including CellPadding) instead of manual AddRectFilled inside content area. Hit-test expanded by CellPadding for proper edge-to-edge coverage. Closes "hover has gap between cell borders".

v1.3.6 (2026-05-15) — Selection (drag-range) also paints via TableSetBgColor — same edge-to-edge coverage as hover. Header/HeaderHovered/HeaderActive colors set to fully transparent so Selectable doesn't paint anything; all cell bg states (hover, selected, selected+hover) go through TableSetBgColor uniformly.

v1.4.0 (2026-05-16) — new renderers: `CategoricalChip` (dot izquierda + text, always visible, replaces hover-only color-on-text for categorical) + `ColorScale` (continuous N-color LERP gradient for numeric cells, configurable `range_min`/`range_max`/`range_stops`/`range_alpha`). New types: `ChipRule{match,color}` + `ColorStop{position,color}` in `data_table_types.h`. TQL roundtrip (emit+apply) for both renderers. 4 headless tests added to `test_column_specs.cpp`.

v1.5.0 (2026-05-17) — per-table state isolation: `stats_mode`, `stats_cache`, `stats_last_*`, `sel_*`, `inspect_*`, `drill_back/forward` movidos de singleton `UiState` interno a `State`. Antes: toggle "Show stats" / drag-select / Inspect row se aplicaban a TODAS las tablas visibles a la vez. Ahora cada tabla los lleva. Conditional color extendido: nuevo enum `ColorRuleKind { CellBg, CategoricalDot, NumericRange }` + campos en `ColorRule` (`dot_alpha`, `dot_radius_px`, `dot_map`, `range_min/max/alpha/stops`). Menu del header autodetecta el modo por tipo de columna (Int/Float → NumericRange; resto → CellBg). CategoricalDot autoasigna colores desde palette de 12 hash-deterministico — no hace falta mapear cada valor a mano. Fix: hover del menu contextual del header invisible cuando las table-styles pisaban `HeaderHovered` a transparente — restaurado dentro del scope del popup. **Breaking en `data_table_types.h`**: ColStop + DrillStep movidos arriba para que State pueda contenerlos por valor; consumidores que redefinian `ColStats` localmente (playground `data_table_logic.h`) eliminados — usar el del registry (`compute_column_stats.h`, ya transitivamente incluido).

---
Promovido desde `cpp/apps/primitives_gallery/playground/tables/data_table.{h,cpp}` — issue 0081-H.
