From 4b0fdbadb427c8d46b4384f32a96d4ab44f8e47d Mon Sep 17 00:00:00 2001 From: os-zhuang Date: Wed, 10 Jun 2026 20:04:44 +0500 Subject: [PATCH] feat(showcase): chart gallery shows only distinctly-renderable chart types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pairs the objectui console build (objectui#1624) that routes dataset widgets to their TRUE chart renderer with a trimmed Chart Gallery (27 → 17 widgets): one widget per family the renderer draws distinctly. Removed the families that only fall back to a near-relative (grouped/stacked/bi-polar bars, stacked-area, step-line, spline, pyramid, bubble) and the dial-less performance variants (kpi/gauge/solid-gauge/bullet, identical to `metric`) — a "sankey" widget that draws bars is worse than not offering it. - chart-gallery.dashboard.ts → 17 canonical widgets (metric ×3 for format variety, then bar/column/horizontal-bar/line/area/pie/donut/funnel/scatter/ radar/treemap/sankey, table/pivot with multiple measures). - coverage.test → "covers every distinctly-renderable ChartType" (allowlist of the fallback-only families, documented for the follow-up `ChartTypeSchema` trim). - .objectui-sha → 76094e0d (vendors the true-chart-type routing). Browser-verified: pie/donut/funnel/line/area/sankey render distinctly; table/pivot render real tables with resolved labels + currency format. turbo build 72/72, test 123/123. objectui@76094e0d4ed79b8732ba9879c75aa5ab8283180d Co-Authored-By: Claude Opus 4.8 --- .changeset/chart-gallery-renderable-types.md | 12 +++ .objectui-sha | 2 +- .../src/dashboards/chart-gallery.dashboard.ts | 86 ++++++++----------- examples/app-showcase/test/coverage.test.ts | 18 +++- 4 files changed, 64 insertions(+), 54 deletions(-) create mode 100644 .changeset/chart-gallery-renderable-types.md diff --git a/.changeset/chart-gallery-renderable-types.md b/.changeset/chart-gallery-renderable-types.md new file mode 100644 index 0000000000..237324c887 --- /dev/null +++ b/.changeset/chart-gallery-renderable-types.md @@ -0,0 +1,12 @@ +--- +"@objectstack/example-showcase": patch +--- + +The showcase Chart Gallery now shows one widget per chart family the renderer +draws DISTINCTLY (27 → 17 widgets). Families that fell back to a near-relative +(grouped/stacked/bi-polar bars, stacked-area, step-line, spline, pyramid, +bubble) and the dial-less performance variants (kpi/gauge/solid-gauge/bullet, +identical to `metric`) were removed — advertising a type that renders as +something else is misleading. Bundles the objectui console build that routes +each widget to its true chart renderer (pie/donut/funnel/line/area/scatter/ +radar/treemap/sankey/table/pivot). diff --git a/.objectui-sha b/.objectui-sha index 6777a5cf30..bbab823b0f 100644 --- a/.objectui-sha +++ b/.objectui-sha @@ -1 +1 @@ -d2972a54a31cb26dc22627faba333a7fb6839101 +76094e0d4ed79b8732ba9879c75aa5ab8283180d diff --git a/examples/app-showcase/src/dashboards/chart-gallery.dashboard.ts b/examples/app-showcase/src/dashboards/chart-gallery.dashboard.ts index 451d2022b0..b06861a669 100644 --- a/examples/app-showcase/src/dashboards/chart-gallery.dashboard.ts +++ b/examples/app-showcase/src/dashboards/chart-gallery.dashboard.ts @@ -2,75 +2,61 @@ import type { Dashboard } from '@objectstack/spec/ui'; -const task = 'showcase_task'; -const project = 'showcase_project'; const taskDs = 'showcase_task_metrics'; const projectDs = 'showcase_project_metrics'; /** - * Chart Gallery — one widget per chart family so the dashboard renderer can be - * exercised against every visualisation type. Covers the full `ChartTypeSchema` - * taxonomy (comparison, trend, distribution, relationship, composition, - * performance, tabular) — every type here renders; the taxonomy intentionally - * excludes families the renderer cannot draw (geo maps, OHLC, distributions). + * Chart Gallery — one widget per chart family the dashboard renderer can draw + * DISTINCTLY, so the showcase honestly reflects what the platform implements. * - * ADR-0021 Phase 2: every widget is bound to a semantic dataset - * (`showcase_task_metrics` / `showcase_project_metrics`) and selects - * dimensions/measures BY NAME, side-by-side with the legacy inline query during - * the dual-form window. The reconciliation harness asserts both forms return - * identical numbers (scripts/analytics-reconcile). Date-bucketed trend widgets - * (`created_at` + `categoryGranularity`) stay inline-only for now — dataset - * timeDimension reconciliation is deferred (see the CRM trend widget). + * Every widget binds a semantic dataset (`showcase_task_metrics` / + * `showcase_project_metrics`, ADR-0021) and selects dimensions/measures BY NAME; + * the analytics layer resolves dimension display labels (select option labels, + * lookup names, month-bucketed dates) and carries each measure's label + format. + * + * Only types with a real, distinct renderer are shown. The chart families that + * currently fall back to a near-relative (grouped/stacked/bi-polar bars → bar, + * stacked-area → area, step-line/spline → line, pyramid → funnel, bubble → + * scatter) and the single-value performance variants without a dial + * (gauge/solid-gauge/bullet, which render the same as `metric`) are intentionally + * NOT duplicated here — advertising a type that renders as something else is + * worse than not offering it. (Follow-up: trim those from `ChartTypeSchema`.) */ export const ChartGalleryDashboard: Dashboard = { name: 'showcase_chart_gallery', label: 'Chart Gallery', - description: 'A representative widget for every chart family — visual coverage of the dashboard renderer.', + description: 'One widget per chart family the renderer draws distinctly — honest visual coverage.', columns: 12, widgets: [ - // ── Performance / KPI ──────────────────────────────────────────────── - { id: 'kpi_total_tasks', type: 'metric', title: 'Total Tasks', dataset: taskDs, values: ['task_count'], layout: { x: 0, y: 0, w: 3, h: 2 } }, - { id: 'kpi_open_tasks', type: 'kpi', title: 'Open Tasks', filter: { done: false }, dataset: taskDs, values: ['task_count'], layout: { x: 3, y: 0, w: 3, h: 2 } }, - { id: 'gauge_progress', type: 'gauge', title: 'Avg Progress', dataset: taskDs, values: ['avg_progress'], layout: { x: 6, y: 0, w: 3, h: 2 } }, - { id: 'bullet_budget', type: 'bullet', title: 'Budget vs Spend', dataset: projectDs, values: ['spent_sum'], layout: { x: 9, y: 0, w: 3, h: 2 } }, + // ── Performance / KPI (one measure value; demonstrates label + format) ── + { id: 'kpi_total_tasks', type: 'metric', title: 'Total Tasks', dataset: taskDs, values: ['task_count'], layout: { x: 0, y: 0, w: 4, h: 2 } }, + { id: 'kpi_avg_progress', type: 'metric', title: 'Avg Progress', dataset: taskDs, values: ['avg_progress'], layout: { x: 4, y: 0, w: 4, h: 2 } }, + { id: 'kpi_total_spent', type: 'metric', title: 'Total Spent', dataset: projectDs, values: ['spent_sum'], layout: { x: 8, y: 0, w: 4, h: 2 } }, - // ── Comparison ─────────────────────────────────────────────────────── + // ── Comparison ───────────────────────────────────────────────────────── { id: 'bar_by_status', type: 'bar', title: 'Tasks by Status', dataset: taskDs, dimensions: ['status'], values: ['task_count'], layout: { x: 0, y: 2, w: 4, h: 4 } }, { id: 'column_by_priority', type: 'column', title: 'Tasks by Priority', dataset: taskDs, dimensions: ['priority'], values: ['task_count'], layout: { x: 4, y: 2, w: 4, h: 4 } }, { id: 'hbar_hours', type: 'horizontal-bar', title: 'Hours by Status', dataset: taskDs, dimensions: ['status'], values: ['est_hours'], layout: { x: 8, y: 2, w: 4, h: 4 } }, - { id: 'stacked_bar', type: 'stacked-bar', title: 'Status × Priority', dataset: taskDs, dimensions: ['status'], values: ['task_count'], layout: { x: 0, y: 6, w: 4, h: 4 } }, - { id: 'grouped_bar', type: 'grouped-bar', title: 'Grouped Status', dataset: taskDs, dimensions: ['status'], values: ['task_count'], layout: { x: 4, y: 6, w: 4, h: 4 } }, - - // ── Trend (date-bucketed via timeDimension granularity) ────────────── - { id: 'line_created', type: 'line', title: 'Tasks Created (monthly)', dataset: taskDs, dimensions: ['created_at'], values: ['task_count'], layout: { x: 8, y: 6, w: 4, h: 4 } }, - { id: 'area_created', type: 'area', title: 'Cumulative (area)', dataset: taskDs, dimensions: ['created_at'], values: ['task_count'], layout: { x: 0, y: 10, w: 4, h: 4 } }, - { id: 'stacked_area', type: 'stacked-area', title: 'Stacked Area', dataset: taskDs, dimensions: ['created_at'], values: ['task_count'], layout: { x: 4, y: 10, w: 4, h: 4 } }, - { id: 'spline_trend', type: 'spline', title: 'Smoothed Trend', dataset: taskDs, dimensions: ['created_at'], values: ['task_count'], layout: { x: 8, y: 10, w: 4, h: 4 } }, - - // ── Distribution ───────────────────────────────────────────────────── - { id: 'pie_status', type: 'pie', title: 'Status Split', dataset: taskDs, dimensions: ['status'], values: ['task_count'], layout: { x: 0, y: 14, w: 3, h: 4 } }, - { id: 'donut_priority', type: 'donut', title: 'Priority Split', dataset: taskDs, dimensions: ['priority'], values: ['task_count'], layout: { x: 3, y: 14, w: 3, h: 4 } }, - { id: 'funnel_pipeline', type: 'funnel', title: 'Task Funnel', dataset: taskDs, dimensions: ['status'], values: ['task_count'], layout: { x: 6, y: 14, w: 3, h: 4 } }, - { id: 'pyramid_priority', type: 'pyramid', title: 'Priority Pyramid', dataset: taskDs, dimensions: ['priority'], values: ['task_count'], layout: { x: 9, y: 14, w: 3, h: 4 } }, - // ── Relationship ───────────────────────────────────────────────────── - { id: 'scatter_estimate', type: 'scatter', title: 'Estimate vs Progress', dataset: taskDs, dimensions: ['progress'], values: ['avg_estimate'], layout: { x: 0, y: 18, w: 4, h: 4 } }, - { id: 'bubble_budget', type: 'bubble', title: 'Budget Bubble', dataset: projectDs, dimensions: ['account'], values: ['budget_sum'], layout: { x: 4, y: 18, w: 4, h: 4 } }, + // ── Trend (month-bucketed via the dataset's created_at granularity) ────── + { id: 'line_created', type: 'line', title: 'Tasks Created (monthly)', dataset: taskDs, dimensions: ['created_at'], values: ['task_count'], layout: { x: 0, y: 6, w: 6, h: 4 } }, + { id: 'area_created', type: 'area', title: 'Tasks Created (area)', dataset: taskDs, dimensions: ['created_at'], values: ['task_count'], layout: { x: 6, y: 6, w: 6, h: 4 } }, - // ── Composition ────────────────────────────────────────────────────── - { id: 'treemap_hours', type: 'treemap', title: 'Hours Treemap', dataset: taskDs, dimensions: ['status'], values: ['est_hours'], layout: { x: 8, y: 18, w: 4, h: 4 } }, - { id: 'sankey_flow', type: 'sankey', title: 'Status Flow (Sankey)', dataset: taskDs, dimensions: ['status'], values: ['task_count'], layout: { x: 0, y: 22, w: 4, h: 4 } }, - { id: 'radar_priority', type: 'radar', title: 'Priority Radar', dataset: taskDs, dimensions: ['priority'], values: ['task_count'], layout: { x: 4, y: 22, w: 4, h: 4 } }, + // ── Distribution ───────────────────────────────────────────────────────── + { id: 'pie_status', type: 'pie', title: 'Status Split', dataset: taskDs, dimensions: ['status'], values: ['task_count'], layout: { x: 0, y: 10, w: 4, h: 4 } }, + { id: 'donut_priority', type: 'donut', title: 'Priority Split', dataset: taskDs, dimensions: ['priority'], values: ['task_count'], layout: { x: 4, y: 10, w: 4, h: 4 } }, + { id: 'funnel_status', type: 'funnel', title: 'Status Funnel', dataset: taskDs, dimensions: ['status'], values: ['task_count'], layout: { x: 8, y: 10, w: 4, h: 4 } }, - // ── Performance ────────────────────────────────────────────────────── - { id: 'solid_gauge', type: 'solid-gauge', title: 'Solid Gauge', dataset: taskDs, values: ['avg_progress'], layout: { x: 8, y: 22, w: 4, h: 4 } }, + // ── Relationship + Advanced ────────────────────────────────────────────── + { id: 'scatter_estimate', type: 'scatter', title: 'Estimate vs Progress', dataset: taskDs, dimensions: ['progress'], values: ['avg_estimate'], layout: { x: 0, y: 14, w: 6, h: 4 } }, + { id: 'radar_priority', type: 'radar', title: 'Priority Radar', dataset: taskDs, dimensions: ['priority'], values: ['task_count'], layout: { x: 6, y: 14, w: 6, h: 4 } }, - // ── Comparison / trend variants ────────────────────────────────────── - { id: 'bipolar_bar', type: 'bi-polar-bar', title: 'Bi-polar Bar', dataset: taskDs, dimensions: ['status'], values: ['task_count'], layout: { x: 0, y: 26, w: 6, h: 4 } }, - { id: 'step_line', type: 'step-line', title: 'Step Line', dataset: taskDs, dimensions: ['created_at'], values: ['task_count'], layout: { x: 6, y: 26, w: 6, h: 4 } }, + // ── Composition ────────────────────────────────────────────────────────── + { id: 'treemap_hours', type: 'treemap', title: 'Hours Treemap', dataset: taskDs, dimensions: ['status'], values: ['est_hours'], layout: { x: 0, y: 18, w: 6, h: 4 } }, + { id: 'sankey_flow', type: 'sankey', title: 'Status Flow (Sankey)', dataset: taskDs, dimensions: ['status'], values: ['task_count'], layout: { x: 6, y: 18, w: 6, h: 4 } }, - // ── Tabular ────────────────────────────────────────────────────────── - { id: 'table_projects', type: 'table', title: 'Projects Table', dataset: projectDs, values: ['project_count'], layout: { x: 0, y: 30, w: 6, h: 4 } }, - { id: 'pivot_tasks', type: 'pivot', title: 'Tasks Pivot', dataset: taskDs, dimensions: ['status'], values: ['task_count'], layout: { x: 6, y: 30, w: 6, h: 4 } }, + // ── Tabular (real grouped tables, multiple measures) ───────────────────── + { id: 'table_projects', type: 'table', title: 'Projects by Account', dataset: projectDs, dimensions: ['account'], values: ['project_count', 'budget_sum', 'spent_sum'], layout: { x: 0, y: 22, w: 6, h: 4 } }, + { id: 'pivot_tasks', type: 'pivot', title: 'Tasks by Status × Priority', dataset: taskDs, dimensions: ['status', 'priority'], values: ['task_count'], layout: { x: 6, y: 22, w: 6, h: 4 } }, ], }; diff --git a/examples/app-showcase/test/coverage.test.ts b/examples/app-showcase/test/coverage.test.ts index 3bf2c58422..dc0b3751d8 100644 --- a/examples/app-showcase/test/coverage.test.ts +++ b/examples/app-showcase/test/coverage.test.ts @@ -48,9 +48,21 @@ describe('showcase coverage (introspected against the spec)', () => { expectFullCoverage('FormViewType', [...FORM_VIEW_TYPES], collectFormViewTypes(views as never)); }); - it('covers every ChartType', () => { - const expected = enumValues(ui.ChartTypeSchema); - expect(expected.length).toBeGreaterThan(20); + it('covers every distinctly-renderable ChartType', () => { + // The Chart Gallery demonstrates only chart families the renderer draws + // DISTINCTLY — advertising a type that renders as something else (a "sankey" + // that draws bars) is worse than not offering it. The families below have no + // distinct renderer yet and fall back to a near-relative, so the showcase + // intentionally does not duplicate them: grouped/stacked/bi-polar bars (→bar), + // stacked-area (→area), step-line/spline (→line), pyramid (→funnel), bubble + // (→scatter), and the dial-less performance variants kpi/gauge/solid-gauge/ + // bullet (→ the same KPI value as `metric`). Follow-up: trim these from + // `ChartTypeSchema` so spec ↔ renderer ↔ showcase stay in lockstep. + const FALLBACK_ONLY = new Set([ + 'grouped-bar', 'stacked-bar', 'bi-polar-bar', 'stacked-area', 'step-line', + 'spline', 'pyramid', 'bubble', 'kpi', 'gauge', 'solid-gauge', 'bullet', + ]); + const expected = enumValues(ui.ChartTypeSchema).filter((t) => !FALLBACK_ONLY.has(t)); const used = new Set(); for (const w of ChartGalleryDashboard.widgets ?? []) if (w.type) used.add(w.type); expectFullCoverage('ChartType', expected, used);