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>
This commit is contained in:
2026-05-15 17:24:53 +02:00
parent 304f48ccac
commit 4acf6986d3
10 changed files with 389 additions and 18 deletions
Submodule cpp/apps/dag_engine_ui updated: f150f96c8f...61314b7f96
+12 -1
View File
@@ -130,6 +130,7 @@ enum class JoinStrategy { Left, Inner, Right, Full };
// ----------------------------------------------------------------------------
// CellRenderer: declarative rendering mode per column (issue 0081-N, v1.1.0).
// Phase 2 (issue 0081-O, v1.2.0): Button=5 added.
// Phase 2.5 (issue 0081-O.5, v1.3.0): Dots=8 added (inline status timeline).
// ----------------------------------------------------------------------------
enum class CellRenderer : uint8_t {
Text = 0, // default — current behavior
@@ -138,7 +139,8 @@ enum class CellRenderer : uint8_t {
Duration = 3, // milliseconds with color gradient
Icon = 4, // icon lookup by value string
Button = 5, // clickable button; emits TableEvent::ButtonClick
// Future (Phase 3): TextInput=6, Custom=7. IDs reserved.
// 6, 7: reserved for Phase 3 (TextInput, Custom).
Dots = 8, // inline dots sparkline; cell = separator-delimited tokens
};
// ----------------------------------------------------------------------------
@@ -203,6 +205,15 @@ struct ColumnSpec {
// Tooltip (Phase 2, v1.2.0): per-cell hover tooltip
std::string tooltip; // text; "auto" -> show cell value
bool tooltip_on_hover = false; // if true, show on hover
// Dots (Phase 2.5, v1.3.0): CellRenderer::Dots — inline status sparkline.
// Cell value is a separator-delimited string of tokens, e.g. "ok,error,ok".
// Each token is looked up in `badges` for color (and optional glyph override via label).
// Default glyph: u8"●"; override by setting BadgeRule.label to another UTF-8 glyph.
char dots_separator = ','; // separator between tokens
float dots_glyph_size = 0.0f; // glyph size px; 0 = default font size
int dots_max = 0; // hard limit on dots shown; 0 = no limit
bool dots_show_count = false; // if true, appends " (N)" after dots
};
// ----------------------------------------------------------------------------
+19 -1
View File
@@ -540,11 +540,12 @@ ApplyResult apply(const std::string& lua_text,
lua_getfield(L, -1, "renderer");
if (lua_isstring(L, -1)) {
std::string rn = lua_tostring(L, -1);
if (rn == "badge") cs.renderer = data_table::CellRenderer::Badge;
if (rn == "badge") cs.renderer = data_table::CellRenderer::Badge;
else if (rn == "progress") cs.renderer = data_table::CellRenderer::Progress;
else if (rn == "duration") cs.renderer = data_table::CellRenderer::Duration;
else if (rn == "icon") cs.renderer = data_table::CellRenderer::Icon;
else if (rn == "button") cs.renderer = data_table::CellRenderer::Button;
else if (rn == "dots") cs.renderer = data_table::CellRenderer::Dots;
else cs.renderer = data_table::CellRenderer::Text;
}
lua_pop(L, 1);
@@ -624,6 +625,23 @@ ApplyResult apply(const std::string& lua_text,
if (lua_isstring(L, -1)) cs.button_color_hex = lua_tostring(L, -1);
lua_pop(L, 1);
// Dots (Phase 2.5, v1.3.0)
lua_getfield(L, -1, "dots_separator");
if (lua_isstring(L, -1)) {
const char* sep_s = lua_tostring(L, -1);
if (sep_s && sep_s[0]) cs.dots_separator = sep_s[0];
}
lua_pop(L, 1);
lua_getfield(L, -1, "dots_max");
if (lua_isnumber(L, -1)) cs.dots_max = (int)lua_tointeger(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "dots_show_count");
if (lua_isboolean(L, -1)) cs.dots_show_count = (bool)lua_toboolean(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "dots_glyph_size");
if (lua_isnumber(L, -1)) cs.dots_glyph_size = (float)lua_tonumber(L, -1);
lua_pop(L, 1);
// Tooltip
lua_getfield(L, -1, "tooltip");
if (lua_isstring(L, -1)) cs.tooltip = lua_tostring(L, -1);
+35
View File
@@ -519,6 +519,40 @@ static void test_roundtrip_column_specs_tooltip() {
check(cs2.tooltip_on_hover == true, "cs tooltip roundtrip: tooltip_on_hover=true");
}
// ---------------------------------------------------------------------------
// Test: column_specs roundtrip — Dots (v1.3.0)
// ---------------------------------------------------------------------------
static void test_roundtrip_column_specs_dots() {
State st;
st.stages.push_back(Stage{});
ColumnSpec cs;
cs.id = "recent";
cs.renderer = CellRenderer::Dots;
cs.badges = {{"ok", "#22c55e", ""}, {"error", "#ef4444", "failed"}};
cs.dots_max = 5;
cs.dots_show_count = false;
cs.tooltip_on_hover = true;
st.aux_column_specs.push_back({cs});
std::vector<std::string> headers = {"name", "recent"};
std::vector<ColumnType> types = {ColumnType::String, ColumnType::String};
std::string tql_text = tql::emit(st, headers, types);
auto res = tql::apply(tql_text, headers);
check(res.ok, "cs dots roundtrip: ok");
check(!res.state.aux_column_specs.empty() &&
!res.state.aux_column_specs[0].empty(), "cs dots roundtrip: specs present");
const auto& cs2 = res.state.aux_column_specs[0][0];
check(cs2.renderer == CellRenderer::Dots, "cs dots roundtrip: renderer=Dots");
check(cs2.dots_max == 5, "cs dots roundtrip: dots_max=5");
check(cs2.tooltip_on_hover == true, "cs dots roundtrip: tooltip_on_hover=true");
check(cs2.badges.size() == 2, "cs dots roundtrip: 2 badge rules");
check(cs2.badges[0].value == "ok", "cs dots roundtrip: badge[0].value=ok");
check(cs2.badges[1].label == "failed", "cs dots roundtrip: badge[1].label=failed");
}
// ---------------------------------------------------------------------------
// main
// ---------------------------------------------------------------------------
@@ -540,6 +574,7 @@ int main() {
test_roundtrip_column_specs_badge();
test_roundtrip_column_specs_button();
test_roundtrip_column_specs_tooltip();
test_roundtrip_column_specs_dots();
std::printf("---\nResults: %d passed, %d failed\n", g_pass, g_fail);
return g_fail == 0 ? 0 : 1;
+19 -1
View File
@@ -283,7 +283,8 @@ std::string emit(const State& state,
// Emit the block only if at least one spec has a non-default renderer OR tooltip.
bool any_renderable = false;
for (const auto& cs : specs) {
if (cs.renderer != data_table::CellRenderer::Text || cs.tooltip_on_hover) {
if (cs.renderer != data_table::CellRenderer::Text || cs.tooltip_on_hover ||
cs.renderer == data_table::CellRenderer::Dots) {
any_renderable = true; break;
}
}
@@ -301,6 +302,7 @@ std::string emit(const State& state,
case data_table::CellRenderer::Duration: rname = "duration"; break;
case data_table::CellRenderer::Icon: rname = "icon"; break;
case data_table::CellRenderer::Button: rname = "button"; break;
case data_table::CellRenderer::Dots: rname = "dots"; break;
default: break;
}
out += ", renderer = " + lua_string_literal(rname);
@@ -352,6 +354,22 @@ std::string emit(const State& state,
if (!cs.button_color_hex.empty())
out += ", button_color = " + lua_string_literal(cs.button_color_hex);
}
// Dots (Phase 2.5, v1.3.0)
if (cs.renderer == data_table::CellRenderer::Dots) {
if (cs.dots_separator != ',') {
char sep_str[2] = {cs.dots_separator, '\0'};
out += ", dots_separator = " + lua_string_literal(sep_str);
}
if (cs.dots_max > 0)
out += ", dots_max = " + std::to_string(cs.dots_max);
if (cs.dots_show_count)
out += ", dots_show_count = true";
if (cs.dots_glyph_size > 0.0f) {
char buf[32];
std::snprintf(buf, sizeof(buf), "%g", (double)cs.dots_glyph_size);
out += std::string(", dots_glyph_size = ") + buf;
}
}
// Tooltip
if (cs.tooltip_on_hover) {
out += ", tooltip = " + lua_string_literal(cs.tooltip.empty() ? "auto" : cs.tooltip);
+30
View File
@@ -306,6 +306,35 @@ static void test_emit_column_specs_tooltip() {
check(contains(out, "tooltip_on_hover = true"), "emit column_specs tooltip: on_hover");
}
// ---------------------------------------------------------------------------
// Test: emit with Dots column_spec in aux_column_specs (v1.3.0)
// ---------------------------------------------------------------------------
static void test_emit_column_specs_dots() {
State st;
st.stages.push_back(Stage{});
ColumnSpec cs;
cs.id = "recent";
cs.renderer = CellRenderer::Dots;
cs.badges = {{"ok", "#22c55e", ""}, {"error", "#ef4444", ""}};
cs.dots_max = 5;
cs.dots_show_count = false;
cs.tooltip_on_hover = true;
st.aux_column_specs.push_back({cs});
std::vector<std::string> headers = {"name", "recent"};
std::vector<ColumnType> types = {ColumnType::String, ColumnType::String};
std::string out = tql::emit(st, headers, types);
check(contains(out, "renderer = \"dots\""), "emit column_specs dots: renderer");
check(contains(out, "id = \"recent\""), "emit column_specs dots: id");
check(contains(out, "dots_max = 5"), "emit column_specs dots: dots_max");
check(contains(out, "tooltip_on_hover = true"), "emit column_specs dots: tooltip_on_hover");
check(contains(out, "\"#22c55e\""), "emit column_specs dots: badge color ok");
check(contains(out, "\"error\""), "emit column_specs dots: badge value error");
}
// ---------------------------------------------------------------------------
// main
// ---------------------------------------------------------------------------
@@ -323,6 +352,7 @@ int main() {
test_emit_column_specs_badge();
test_emit_column_specs_button();
test_emit_column_specs_tooltip();
test_emit_column_specs_dots();
std::printf("---\nResults: %d passed, %d failed\n", g_pass, g_fail);
return g_fail == 0 ? 0 : 1;
+59 -2
View File
@@ -327,15 +327,72 @@ static void draw_cell_custom(const ColumnSpec& spec, const char* value,
break;
}
case CellRenderer::Dots: {
// Parse cell value as separator-delimited tokens.
std::string s = value; // value is guaranteed non-null (checked at top)
std::vector<std::string> 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();
for (int t = 0; t < limit; ++t) {
if (t > 0) ImGui::SameLine(0, 2.0f); // tight spacing between dots
// Look up color + optional glyph override in badges.
ImVec4 color(0.4f, 0.4f, 0.45f, 1.0f); // default: muted grey
const char* glyph = u8"\xe2\x97\x8f"; // UTF-8 for ●
for (const auto& br : spec.badges) {
if (br.value == tokens[t]) {
ImVec4 c = hex_to_imcolor(br.color_hex);
if (c.x >= 0.f) color = c;
if (!br.label.empty()) glyph = br.label.c_str();
break;
}
}
// Optional glyph size push.
bool pushed_font_scale = false;
if (spec.dots_glyph_size > 0.0f) {
float scale = spec.dots_glyph_size / ImGui::GetFontSize();
ImGui::SetWindowFontScale(scale);
pushed_font_scale = true;
}
ImGui::TextColored(color, "%s", glyph);
if (pushed_font_scale) ImGui::SetWindowFontScale(1.0f);
// Per-dot tooltip: show the token string.
if (spec.tooltip_on_hover && ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", tokens[t].c_str());
}
}
if (spec.dots_show_count) {
ImGui::SameLine(0, 6.0f);
ImGui::TextDisabled("(%d)", (int)tokens.size());
}
break;
}
default:
// CellRenderer::Text or unknown — plain text.
ImGui::TextUnformatted(value);
break;
}
// Tooltip: show on hover if tooltip_on_hover is set.
// Tooltip: show on hover if tooltip_on_hover is set (non-Dots renderers).
// For Dots, per-dot tooltips are handled inline above.
// "auto" shows the raw cell value (useful for truncated text columns).
if (spec.tooltip_on_hover && ImGui::IsItemHovered()) {
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);
}
+6 -2
View File
@@ -3,10 +3,10 @@ name: data_table
kind: function
lang: cpp
domain: viz
version: "1.2.0"
version: "1.3.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. Entry-point publica del stack data_table. Muta State segun interaccion del usuario."
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). 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
@@ -70,6 +70,8 @@ tests:
- "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"
test_file_path: "cpp/tests/test_column_specs.cpp"
file_path: "cpp/functions/viz/data_table.cpp"
params:
@@ -180,5 +182,7 @@ v1.1.0 (2026-05-15) — declarative CellRenderer (Badge/Progress/Duration/Icon)
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).
---
Promovido desde `cpp/apps/primitives_gallery/playground/tables/data_table.{h,cpp}` — issue 0081-H.
+94 -1
View File
@@ -1,5 +1,6 @@
// test_column_specs.cpp — Smoke / back-compat tests for declarative cell renderers.
// Issue 0081-N, v1.1.0. Phase 2 (issue 0081-O, v1.2.0).
// Phase 2.5 (issue 0081-O.5, v1.3.0): Dots renderer.
//
// These tests verify:
// 1. TableInput without column_specs compiles and links (back-compat).
@@ -7,6 +8,7 @@
// 6. Button renderer: TableEvent struct is constructible; events_out pointer accepted.
// 7. Tooltip field: ColumnSpec with tooltip_on_hover=true compiles and links.
// 8. render() overload with events_out=nullptr back-compat (symbol resolution only).
// 9. Dots renderer: ColumnSpec with CellRenderer::Dots + badges constructs correctly.
//
// None of these tests call data_table::render() (requires ImGui context).
// They only verify that the new types are usable and that the symbols from
@@ -305,6 +307,95 @@ static void test_render_backcompat_overload() {
std::printf("PASS: test_render_backcompat_overload (both render() signatures link)\n");
}
// ---------------------------------------------------------------------------
// Test 9: Dots renderer — ColumnSpec with CellRenderer::Dots + badges.
// Cell value "ok,error,ok,running,ok" — 5 tokens, 5 badge rules, dots_max=5.
// ---------------------------------------------------------------------------
static void test_dots_column_spec() {
// Dataset with one column holding comma-separated run statuses.
static const char* cells_dots[] = {
"ok,error,ok,running,ok",
"ok,ok,ok",
"failed,failed",
};
std::vector<std::string> hdrs = {"recent"};
std::vector<ColumnType> types = {ColumnType::String};
TableInput t;
t.name = "t_dots";
t.rows = 3;
t.cols = 1;
t.cells = cells_dots;
t.headers = hdrs;
t.types = types;
ColumnSpec cs;
cs.id = "recent";
cs.renderer = CellRenderer::Dots;
cs.badges = {
BadgeRule{"ok", "#22c55e", ""}, // default glyph ●
BadgeRule{"error", "#ef4444", ""},
BadgeRule{"failed", "#ef4444", ""},
BadgeRule{"running", "#eab308", ""},
BadgeRule{"pending", "#94a3b8", ""},
};
cs.dots_max = 5;
cs.dots_show_count = false;
cs.tooltip_on_hover = true;
t.column_specs.resize(1);
t.column_specs[0] = cs;
assert(t.column_specs[0].renderer == CellRenderer::Dots);
assert(t.column_specs[0].dots_max == 5);
assert(t.column_specs[0].badges.size() == 5);
assert(t.column_specs[0].dots_show_count == false);
assert(t.column_specs[0].tooltip_on_hover == true);
assert(t.column_specs[0].dots_separator == ',');
// Verify that the struct is copyable and the enum value is distinct.
ColumnSpec cs2 = cs;
assert(cs2.renderer == CellRenderer::Dots);
assert(cs2.dots_max == 5);
std::printf("PASS: test_dots_column_spec "
"(5 badge rules, dots_max=5, tooltip_on_hover=true)\n");
}
// ---------------------------------------------------------------------------
// Test 10: Dots TQL roundtrip — emit + apply preserves Dots fields.
// ---------------------------------------------------------------------------
static void test_dots_tql_roundtrip() {
// Import tql::emit / tql::apply via the smoke test include paths.
// This test checks that the new Dots fields survive a TQL roundtrip.
// We populate aux_column_specs[0] and verify apply reconstructs them.
// We only check struct construction + enum identity here (no Lua context
// available without the full fn_table_viz library; the roundtrip is covered
// by test_fn_table_viz_smoke.cpp test_tql_roundtrip).
ColumnSpec cs;
cs.id = "recent";
cs.renderer = CellRenderer::Dots;
cs.dots_separator = ',';
cs.dots_max = 5;
cs.dots_show_count = false;
cs.badges = {{"ok", "#22c55e", ""}, {"error", "#ef4444", ""}};
// Build a State with aux_column_specs to verify the container accepts Dots.
data_table::State st;
st.stages.push_back(data_table::Stage{});
st.aux_column_specs.push_back({cs});
assert(st.aux_column_specs.size() == 1);
assert(st.aux_column_specs[0].size() == 1);
assert(st.aux_column_specs[0][0].renderer == CellRenderer::Dots);
assert(st.aux_column_specs[0][0].dots_max == 5);
assert(st.aux_column_specs[0][0].badges.size() == 2);
std::printf("PASS: test_dots_tql_roundtrip "
"(State::aux_column_specs accepts Dots spec)\n");
}
// ---------------------------------------------------------------------------
// main
// ---------------------------------------------------------------------------
@@ -318,6 +409,8 @@ int main() {
test_button_column_spec_and_event_struct();
test_tooltip_column_spec();
test_render_backcompat_overload();
std::printf("=== ALL TESTS PASSED (8/8) ===\n");
test_dots_column_spec();
test_dots_tql_roundtrip();
std::printf("=== ALL TESTS PASSED (10/10) ===\n");
return 0;
}
+114 -9
View File
@@ -1,21 +1,45 @@
# data_table_renderers — declarative cell renderers (v1.2.0)
# 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 y **Button** (Phase 2), 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.
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`** |
| `BadgeRule` | value (exact match) + color_hex + label opcional |
| `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** |
| `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 |
@@ -182,7 +206,88 @@ data_table::render("##t", {t}, res.state, &events);
- `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):
```cpp
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` (8 tests: 1 back-compat + 4 renderer types + 3 Phase 2). Smoke/linker; no requieren ImGui context.
- 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:**
```cpp
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:**
```cpp
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.