From 5974484bd44f18b2c6e7bbd145b248317c7440fa Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Fri, 15 May 2026 17:24:53 +0200 Subject: [PATCH] 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 --- cpp/apps/dag_engine_ui | 2 +- cpp/functions/core/data_table_types.h | 13 ++- cpp/functions/core/tql_apply.cpp | 20 +++- cpp/functions/core/tql_apply_test.cpp | 35 ++++++ cpp/functions/core/tql_emit.cpp | 20 +++- cpp/functions/core/tql_emit_test.cpp | 30 ++++++ cpp/functions/viz/data_table.cpp | 61 ++++++++++- cpp/functions/viz/data_table.md | 8 +- cpp/tests/test_column_specs.cpp | 95 ++++++++++++++++- docs/capabilities/data_table_renderers.md | 123 ++++++++++++++++++++-- 10 files changed, 389 insertions(+), 18 deletions(-) diff --git a/cpp/apps/dag_engine_ui b/cpp/apps/dag_engine_ui index f150f96c..61314b7f 160000 --- a/cpp/apps/dag_engine_ui +++ b/cpp/apps/dag_engine_ui @@ -1 +1 @@ -Subproject commit f150f96c8fc58b7d5da4113fd8b301a73b106358 +Subproject commit 61314b7f96a96ca3af7a806922a68a2f603ea83c diff --git a/cpp/functions/core/data_table_types.h b/cpp/functions/core/data_table_types.h index f1ef0882..ebc935a4 100644 --- a/cpp/functions/core/data_table_types.h +++ b/cpp/functions/core/data_table_types.h @@ -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 }; // ---------------------------------------------------------------------------- diff --git a/cpp/functions/core/tql_apply.cpp b/cpp/functions/core/tql_apply.cpp index ff7edb5c..8875dba5 100644 --- a/cpp/functions/core/tql_apply.cpp +++ b/cpp/functions/core/tql_apply.cpp @@ -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); diff --git a/cpp/functions/core/tql_apply_test.cpp b/cpp/functions/core/tql_apply_test.cpp index 9ce57201..eada9f8e 100644 --- a/cpp/functions/core/tql_apply_test.cpp +++ b/cpp/functions/core/tql_apply_test.cpp @@ -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 headers = {"name", "recent"}; + std::vector 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; diff --git a/cpp/functions/core/tql_emit.cpp b/cpp/functions/core/tql_emit.cpp index 0ab7bd73..fc4f90cd 100644 --- a/cpp/functions/core/tql_emit.cpp +++ b/cpp/functions/core/tql_emit.cpp @@ -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); diff --git a/cpp/functions/core/tql_emit_test.cpp b/cpp/functions/core/tql_emit_test.cpp index d0421dba..8bf574f8 100644 --- a/cpp/functions/core/tql_emit_test.cpp +++ b/cpp/functions/core/tql_emit_test.cpp @@ -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 headers = {"name", "recent"}; + std::vector 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; diff --git a/cpp/functions/viz/data_table.cpp b/cpp/functions/viz/data_table.cpp index 6497f671..8d43615d 100644 --- a/cpp/functions/viz/data_table.cpp +++ b/cpp/functions/viz/data_table.cpp @@ -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 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); } diff --git a/cpp/functions/viz/data_table.md b/cpp/functions/viz/data_table.md index 1c4f2613..d4bf283a 100644 --- a/cpp/functions/viz/data_table.md +++ b/cpp/functions/viz/data_table.md @@ -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& tables, State& st, std::vector* 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. diff --git a/cpp/tests/test_column_specs.cpp b/cpp/tests/test_column_specs.cpp index a10ac0f2..77de4f77 100644 --- a/cpp/tests/test_column_specs.cpp +++ b/cpp/tests/test_column_specs.cpp @@ -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 hdrs = {"recent"}; + std::vector 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; } diff --git a/docs/capabilities/data_table_renderers.md b/docs/capabilities/data_table_renderers.md index 7c2baf53..50e166f0 100644 --- a/docs/capabilities/data_table_renderers.md +++ b/docs/capabilities/data_table_renderers.md @@ -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` 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.