Files
fn_registry/docs/capabilities/data_table_renderers.md
T
egutierrez 4acf6986d3 data_table v1.3.0: Dots renderer for status timelines + fix dag_engine_ui antipattern + pitfalls doc (issue 0081-O.5)
PARTE A - CellRenderer::Dots (v1.3.0):
- Add Dots=8 to CellRenderer enum (data_table_types.h)
- Add dots_separator/dots_max/dots_show_count/dots_glyph_size fields to ColumnSpec
- Implement draw_cell_custom case Dots in data_table.cpp
  - Parses comma-separated cell value into tokens
  - Looks up each token in badges for color + optional glyph override
  - Per-dot tooltip via tooltip_on_hover
- tql_emit: serialize renderer="dots" + dots_max/dots_show_count/dots_glyph_size/dots_separator
- tql_apply: deserialize all Dots fields
- tql_emit_test: +6 assertions (58 total, 0 failed)
- tql_apply_test: +8 assertions (114 total, 0 failed)
- test_column_specs: +2 tests (10/10 pass)

PARTE B - dag_engine_ui fix: 10 cols -> 6 cols (submodule commit 61314b7)

PARTE C - docs/capabilities/data_table_renderers.md:
- Update to v1.3.0
- Add decision tree for renderer selection
- Add CellRenderer::Dots section with canonical example
- Add Common pitfalls section (multiple columns, badge for free-text, etc.)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:24:53 +02:00

12 KiB

data_table_renderers — declarative cell renderers (v1.3.0)

Tag: cpp-tables (mismo grupo que TQL; los renderers son parte del stack data_table).

Extiende data_table_cpp_viz con una API declarativa para renderizar columnas con Badge, Progress, Duration, Icon, Button (Phase 2) y Dots (Phase 2.5), emitir eventos de interaccion (ButtonClick, RowDoubleClick, RowRightClick), mostrar tooltips por celda y persistir los specs en TQL (aux_column_specs roundtrip). Back-compat 100%: apps sin column_specs ni events_out no necesitan cambios.

Decision tree: que renderer elegir?

Que muestra la celda?
|
+-- Texto libre arbitrario             -> Text (default)
|
+-- Enum-like status discreto (1 val)
|   |
|   +-- Solo color de fondo            -> Badge
|   +-- Necesita icono                 -> Icon
|   +-- Es accion clickable            -> Button
|
+-- Lista/secuencia de status          -> Dots
|
+-- Numero 0..1 o 0..100              -> Progress
|
+-- Milisegundos con threshold         -> Duration
|
+-- Editable inline                    -> TextInput (Fase 3)
|
+-- Widget propio fuera del set        -> Custom (Fase 3, escape hatch)

Tipos nuevos / actualizados en data_table_types.h

Tipo Que es
CellRenderer enum class: Text=0, Badge=1, Progress=2, Duration=3, Icon=4, Button=5, Dots=8
BadgeRule value (exact match) + color_hex + label opcional (usado como glyph override en Dots)
IconMapEntry value + icon_name (ej. "TI_BOLT") + color_hex opcional
ColumnSpec id + renderer + badges / progress / duration / icon_map / button_action, button_label, button_color_hex / tooltip, tooltip_on_hover / dots_separator, dots_max, dots_show_count, dots_glyph_size
TableInput::column_specs std::vector<ColumnSpec> sidecar opcional (size 0 o == cols)
TableEventKind enum class: ButtonClick=1, RowDoubleClick=2, RowRightClick=3, CellEdit=4 (reservado)
TableEvent kind + row + col + column_id + action_id + value
State::aux_column_specs specs persistidos en TQL (roundtrip via tql_emit/tql_apply)

Ejemplo canonico: Recent Executions (status Badge + duration Duration)

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

// --- Datos (owner externo) ---
static const char* g_exec_cells[] = {
    "ok",    "142",
    "error", "3850",
    "ok",    "72",
    "warn",  "1100",
};

// --- Setup (una vez, en init de la app o antes del loop) ---
data_table::TableInput t;
t.name    = "executions";
t.rows    = 4;
t.cols    = 2;
t.cells   = g_exec_cells;
t.headers = {"status", "duration_ms"};
t.types   = {data_table::ColumnType::String, data_table::ColumnType::Float};

// Columna 0: Badge por valor de status
data_table::ColumnSpec cs_status;
cs_status.id       = "status";
cs_status.renderer = data_table::CellRenderer::Badge;
cs_status.badges   = {
    data_table::BadgeRule{"ok",    "#22c55e", "OK"},
    data_table::BadgeRule{"error", "#ef4444", "ERROR"},
    data_table::BadgeRule{"warn",  "#f59e0b", "WARN"},
};

// Columna 1: Duration con gradiente verde/amarillo/rojo
data_table::ColumnSpec cs_dur;
cs_dur.id                = "duration_ms";
cs_dur.renderer          = data_table::CellRenderer::Duration;
cs_dur.duration_warn_ms  = 500.0f;
cs_dur.duration_error_ms = 2000.0f;

t.column_specs = {cs_status, cs_dur};

data_table::State st;  // persiste entre frames

// --- Render loop ---
ImGui::Begin("Executions");
ImGui::BeginChild("##exec_tbl", ImVec2(-1, -1));
data_table::render("##exec", {t}, st);
ImGui::EndChild();
ImGui::End();

Ejemplo: Progress bar + Icon

data_table::ColumnSpec cs_progress;
cs_progress.id                 = "completion";
cs_progress.renderer           = data_table::CellRenderer::Progress;
cs_progress.progress_scale_100 = true;   // cell value es 0..100
cs_progress.progress_color_hex = "#3b82f6";  // azul; "" -> ImPlot auto

data_table::ColumnSpec cs_icon;
cs_icon.id       = "kind";
cs_icon.renderer = data_table::CellRenderer::Icon;
cs_icon.icon_map = {
    data_table::IconMapEntry{"fn",   "TI_BOLT",     "#3b82f6"},
    data_table::IconMapEntry{"type", "TI_DATABASE", ""},
    data_table::IconMapEntry{"app",  "TI_SETTINGS", "#6b7280"},
};

t.column_specs = {cs_progress, cs_icon};

Iconos soportados en CellRenderer::Icon

El lookup es una tabla estatica de ~30 nombres frecuentes:

TI_CHECK    TI_X           TI_ALERT_CIRCLE   TI_CIRCLE_DOT
TI_CLOCK    TI_LOADER      TI_BAN            TI_PLAYER_PLAY
TI_PLAYER_PAUSE  TI_PLAYER_STOP  TI_DATABASE  TI_SETTINGS
TI_USER     TI_USERS       TI_FILE           TI_FOLDER
TI_REFRESH  TI_BOLT        TI_INFO_CIRCLE    TI_ARROW_UP
TI_ARROW_DOWN  TI_ARROW_RIGHT  TI_ARROW_LEFT  TI_DOTS
TI_EYE      TI_EYE_OFF     TI_EDIT          TI_TRASH
TI_COPY     TI_EXTERNAL_LINK

Si el icon_name no esta en la tabla, la celda se renderiza como texto plano.

Ejemplo Phase 2: Button + events + tooltip

// --- Setup ---
t.column_specs.resize(t.cols);

// Columna "actions": boton Cancel por fila
t.column_specs[col_actions].renderer     = data_table::CellRenderer::Button;
t.column_specs[col_actions].button_action = "cancel";
t.column_specs[col_actions].button_label  = "Cancel";
t.column_specs[col_actions].button_color_hex = "#ef4444";

// Columna "status": tooltip automatico (muestra valor truncado)
t.column_specs[col_status].tooltip         = "auto";
t.column_specs[col_status].tooltip_on_hover = true;

// --- Render loop ---
events.clear();
data_table::render("##t", {t}, st, &events);

// --- Procesar eventos ---
for (const auto& ev : events) {
    if (ev.kind == data_table::TableEventKind::ButtonClick &&
        ev.action_id == "cancel") {
        cancel_row(ev.row);
    }
    if (ev.kind == data_table::TableEventKind::RowDoubleClick) {
        open_detail(ev.row);
    }
    if (ev.kind == data_table::TableEventKind::RowRightClick) {
        // Abrir menu propio via ImGui::BeginPopup
        ctx_menu_row = ev.row;
        ImGui::OpenPopup("##ctx");
    }
}

Ejemplo Phase 2: TQL roundtrip de column_specs

// Persistir specs en State.aux_column_specs para guardar en .tql
data_table::ColumnSpec cs;
cs.id = "status"; cs.renderer = data_table::CellRenderer::Badge;
cs.badges = {{"ok","#22c55e","OK"},{"error","#ef4444",""}};
st.aux_column_specs = {{cs}};  // [tabla_0: {spec_col_0}]

// tql_emit serializa aux_column_specs automaticamente
std::string tql = tql::emit(st, headers, types);
// tql_apply lo recupera en res.state.aux_column_specs
auto res = tql::apply(tql, headers);
// render() lo aplica si TableInput.column_specs esta vacio
data_table::render("##t", {t}, res.state, &events);

Fronteras

  • Solo Column 0..N posicional: column_specs[i] aplica a la columna en posicion i del TableInput original. No se mapea por nombre.
  • Button con celda vacia: si el cell value es vacio, NO se dibuja boton. Poner un valor no vacio en la celda para habilitar el boton.
  • No implementa TextInput/Custom (Phase 3 separado).
  • Stage N (agregado): los renderers se aplican por posicion de columna del output agregado — si el breakout cambia el numero de columnas, revisar los indices.
  • RowRightClick: en el raw table (stage 0) el evento se emite pero el popup de drill-down interno sigue abriendose. La app puede abrir su propio popup al detectar el evento.

Gotchas

  • column_specs.size() debe ser 0 (sin specs) o igual a t.cols. Mezcla de tamaños puede causar out-of-bounds silencioso (el render hace c < column_specs.size() guard, pero es mejor ser explicitoo).
  • hex_to_imcolor acepta "#rrggbb" o "rrggbb". Alpha siempre 1.0. Sin soporte para rgba.
  • El ColorRule existente de State (st.color_rules) sigue funcionando — ambos sistemas coexisten. Si hay conflicto, column_specs toma prioridad para el contenido de la celda; color_rules pinta el fondo via TableSetBgColor.
  • En el renderer Badge el Selectable con background coloreado consume el item para hover/click — la seleccion de rango con drag puede verse afectada visualmente en columnas Badge.
  • events_out no se limpia en render() — el caller debe llamar events.clear() antes de cada frame.
  • aux_column_specs solo se persiste para tables[0] (el main table). Specs para tablas extra deben gestionarse por el caller.

CellRenderer::Dots — timeline inline

Para sparkline-like de status (ultimas N ejecuciones, salud de checks, eventos):

ColumnSpec recent;
recent.id       = "recent";
recent.renderer = CellRenderer::Dots;
recent.badges   = { {"success", "#22c55e"}, {"failed", "#ef4444"},
                    {"running", "#eab308"}, {"pending", "#94a3b8"} };
recent.dots_max         = 5;
recent.tooltip_on_hover = true;  // hover sobre cada dot muestra el status string

// Cell value: comma-separated status strings (most-recent first)
// "success,success,failed,running,pending"

Resultado visual: ● ● ● ● ● (verde verde rojo amarillo gris).

Campos de ColumnSpec para Dots:

Campo Default Descripcion
dots_separator ',' Caracter separador de tokens en el cell value
dots_max 0 (sin limite) Limite hard de dots a mostrar
dots_show_count false Si true, añade (N) al final con el total de tokens
dots_glyph_size 0.0 Tamaño en px del glyph; 0 = tamaño de fuente por defecto

Color lookup: igual que Badge — cada token se busca exactamente en badges.value. Sin match: gris tenue #666673. Glyph por defecto: (U+25CF). Para overridearlo, poner el glyph UTF-8 en BadgeRule.label.

TQL: renderer = "dots", campos opcionales dots_separator, dots_max, dots_show_count, dots_glyph_size.

Notas

  • Tests: cpp/tests/test_column_specs.cpp (10 tests: 1 back-compat + 4 renderer types + 3 Phase 2 + 2 Dots Phase 2.5). Smoke/linker; no requieren ImGui context.
  • TQL roundtrip implementado en Phase 2 (issue 0081-O, v1.2.0): tql_emit_test (3 tests) + tql_apply_test (9 tests nuevos).
  • Dots TQL roundtrip cubierto en test_column_specs.cpp test 10 (struct + aux_column_specs).

Common pitfalls

Multiple columns for a status timeline

Incorrecto:

ti.headers = {"Name", "R1", "R2", "R3", "R4", "R5", ...};
for (int i = 1; i <= 5; ++i)
    ti.column_specs[i].renderer = CellRenderer::Badge;

Problemas:

  • 5 columnas separadas = 5x sort headers + 5x filter chips + filas desalineadas.
  • Cells "-" para rellenar cuando hay menos de 5 runs.
  • Imposible filtrar por "muestra DAGs con >=1 failure en ultimas 5".
  • Bug real: dag_engine_ui v1 (ver issue 0081-O.5).

Correcto:

ColumnSpec recent;
recent.renderer = CellRenderer::Dots;
recent.badges   = { {"success","#22c55e"}, {"failed","#ef4444"}, ... };
ti.headers = {"Name", "Recent", ...};
// Cell value: "success,failed,success,running,pending"

Una sola columna, N dots inline. Tooltip por dot muestra el status concreto. Filtrable como string (Recent contains failed).

Badge para texto libre

Badge espera valores discretos (enum-like: ok/error/running). Para texto libre con muchos valores unicos: Text + opcionalmente Custom renderer (Fase 3).

Buttons para navegacion cuando RowDoubleClick es suficiente

Si la accion es "abrir detalle del row", no uses Button por row. Usa RowDoubleClick event + handler comun. Mas limpio y sin IDs de ImGui por fila.

Progress con valores fuera de 0..1

Progress espera 0..1. Si tu valor es 0..100, set progress_scale_100 = true o normaliza antes.

Color hardcodeado fuera de column_specs

El coloreado debe vivir en column_specs[i].badges, no en if (status=="ok") PushStyleColor(GREEN) antes de TextUnformatted. Esto compila pero rompe la promesa declarativa: el TQL serializado no tendra la info y Ask AI no podra razonar sobre la tabla.