From 801ade73d7a3c6bdd44b7575d6fb8160b63302a6 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Mon, 1 Jun 2026 22:03:17 +0800 Subject: [PATCH] fix(spec,showcase): trim ChartType taxonomy to renderable types; bump pin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ChartType taxonomy advertised 38 families, but the Console can only draw ~27 of them — the rest need data shapes the platform doesn't model (OHLC for candlestick/stock, per-record distributions for box-plot/violin), geographic data (choropleth/bubble-map/gl-map), or a renderer the default Recharts engine lacks (sunburst, heatmap, word-cloud, waterfall). Advertising a chart type that can't render is worse than not offering it. - spec: remove the 11 unrenderable types from ChartTypeSchema (keep treemap & sankey — now rendered via Recharts). Update chart/dashboard/report tests; add a guard asserting the dropped types are rejected. - showcase: drop the corresponding chart-gallery widgets; coverage test now asserts the gallery covers exactly the trimmed taxonomy. - bump .objectui-sha → 5265dd31 (treemap/sankey renderer + field-coverage guard). Removed families can return via an opt-in chart renderer plugin once there is real demand and a data model to back them. Co-Authored-By: Claude Opus 4.8 --- .objectui-sha | 2 +- .../src/dashboards/chart-gallery.dashboard.ts | 43 ++++++++----------- examples/app-showcase/test/coverage.test.ts | 2 +- packages/spec/src/ui/chart.test.ts | 41 +++++++++--------- packages/spec/src/ui/chart.zod.ts | 27 +++++------- packages/spec/src/ui/dashboard.test.ts | 2 +- packages/spec/src/ui/report.test.ts | 2 +- 7 files changed, 53 insertions(+), 66 deletions(-) diff --git a/.objectui-sha b/.objectui-sha index 204a78452..e341da0c4 100644 --- a/.objectui-sha +++ b/.objectui-sha @@ -1 +1 @@ -3442c5547328cb5bbbd7ef8a2e704b0d4f0268f3 +5265dd315b6b5e1661c7d6da67bfd4759b2f0dee diff --git a/examples/app-showcase/src/dashboards/chart-gallery.dashboard.ts b/examples/app-showcase/src/dashboards/chart-gallery.dashboard.ts index d22c98595..fef384af2 100644 --- a/examples/app-showcase/src/dashboards/chart-gallery.dashboard.ts +++ b/examples/app-showcase/src/dashboards/chart-gallery.dashboard.ts @@ -6,11 +6,11 @@ const task = 'showcase_task'; const project = 'showcase_project'; /** - * Chart Gallery — one widget per chart family so the dashboard renderer can - * be exercised against every visualisation type. `type` accepts the full - * `ChartTypeSchema` taxonomy (38 members); this dashboard covers a - * representative widget for each category (comparison, trend, distribution, - * relationship, composition, performance, tabular). + * 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). */ export const ChartGalleryDashboard: Dashboard = { name: 'showcase_chart_gallery', @@ -46,30 +46,21 @@ export const ChartGalleryDashboard: Dashboard = { // ── Relationship ───────────────────────────────────────────────────── { id: 'scatter_estimate', type: 'scatter', title: 'Estimate vs Progress', object: task, aggregate: 'avg', valueField: 'estimate_hours', categoryField: 'progress', layout: { x: 0, y: 18, w: 4, h: 4 } }, { id: 'bubble_budget', type: 'bubble', title: 'Budget Bubble', object: project, aggregate: 'sum', valueField: 'budget', categoryField: 'account', layout: { x: 4, y: 18, w: 4, h: 4 } }, - { id: 'heatmap_load', type: 'heatmap', title: 'Load Heatmap', object: task, aggregate: 'count', categoryField: 'status', layout: { x: 8, y: 18, w: 4, h: 4 } }, // ── Composition ────────────────────────────────────────────────────── - { id: 'treemap_hours', type: 'treemap', title: 'Hours Treemap', object: task, aggregate: 'sum', valueField: 'estimate_hours', categoryField: 'status', layout: { x: 0, y: 22, w: 4, h: 4 } }, - { id: 'sunburst_status', type: 'sunburst', title: 'Status Sunburst', object: task, aggregate: 'count', categoryField: 'status', layout: { x: 4, y: 22, w: 4, h: 4 } }, - { id: 'sankey_flow', type: 'sankey', title: 'Status Flow (Sankey)', object: task, aggregate: 'count', categoryField: 'status', layout: { x: 8, y: 26, w: 4, h: 4 } }, - { id: 'radar_priority', type: 'radar', title: 'Priority Radar', object: task, aggregate: 'count', categoryField: 'priority', layout: { x: 8, y: 22, w: 4, h: 4 } }, - { id: 'waterfall_budget', type: 'waterfall', title: 'Budget Waterfall', object: project, aggregate: 'sum', valueField: 'budget', categoryField: 'status', layout: { x: 0, y: 26, w: 6, h: 4 } }, + { id: 'treemap_hours', type: 'treemap', title: 'Hours Treemap', object: task, aggregate: 'sum', valueField: 'estimate_hours', categoryField: 'status', layout: { x: 8, y: 18, w: 4, h: 4 } }, + { id: 'sankey_flow', type: 'sankey', title: 'Status Flow (Sankey)', object: task, aggregate: 'count', categoryField: 'status', layout: { x: 0, y: 22, w: 4, h: 4 } }, + { id: 'radar_priority', type: 'radar', title: 'Priority Radar', object: task, aggregate: 'count', categoryField: 'priority', layout: { x: 4, y: 22, w: 4, h: 4 } }, - // ── Tabular ────────────────────────────────────────────────────────── - { id: 'table_projects', type: 'table', title: 'Projects Table', object: project, aggregate: 'count', layout: { x: 6, y: 26, w: 6, h: 4 } }, - { id: 'pivot_tasks', type: 'pivot', title: 'Tasks Pivot', object: task, aggregate: 'count', categoryField: 'status', layout: { x: 0, y: 30, w: 12, h: 4 } }, + // ── Performance ────────────────────────────────────────────────────── + { id: 'solid_gauge', type: 'solid-gauge', title: 'Solid Gauge', object: task, aggregate: 'avg', valueField: 'progress', layout: { x: 8, y: 22, w: 4, h: 4 } }, + + // ── Comparison / trend variants ────────────────────────────────────── + { id: 'bipolar_bar', type: 'bi-polar-bar', title: 'Bi-polar Bar', object: task, aggregate: 'count', categoryField: 'status', layout: { x: 0, y: 26, w: 6, h: 4 } }, + { id: 'step_line', type: 'step-line', title: 'Step Line', object: task, aggregate: 'count', categoryField: 'created_at', categoryGranularity: 'week', layout: { x: 6, y: 26, w: 6, h: 4 } }, - // ── Remaining chart families (full ChartType coverage) ─────────────── - { id: 'bipolar_bar', type: 'bi-polar-bar', title: 'Bi-polar Bar', object: task, aggregate: 'count', categoryField: 'status', layout: { x: 0, y: 34, w: 3, h: 4 } }, - { id: 'step_line', type: 'step-line', title: 'Step Line', object: task, aggregate: 'count', categoryField: 'created_at', categoryGranularity: 'week', layout: { x: 3, y: 34, w: 3, h: 4 } }, - { id: 'solid_gauge', type: 'solid-gauge', title: 'Solid Gauge', object: task, aggregate: 'avg', valueField: 'progress', layout: { x: 6, y: 34, w: 3, h: 4 } }, - { id: 'word_cloud', type: 'word-cloud', title: 'Label Cloud', object: task, aggregate: 'count', categoryField: 'labels', layout: { x: 9, y: 34, w: 3, h: 4 } }, - { id: 'choropleth', type: 'choropleth', title: 'Choropleth', object: task, aggregate: 'count', categoryField: 'location', layout: { x: 0, y: 38, w: 4, h: 4 } }, - { id: 'bubble_map', type: 'bubble-map', title: 'Bubble Map', object: task, aggregate: 'count', categoryField: 'location', layout: { x: 4, y: 38, w: 4, h: 4 } }, - { id: 'gl_map', type: 'gl-map', title: 'GL Map', object: task, aggregate: 'count', categoryField: 'location', layout: { x: 8, y: 38, w: 4, h: 4 } }, - { id: 'box_plot', type: 'box-plot', title: 'Estimate Box Plot', object: task, aggregate: 'avg', valueField: 'estimate_hours', categoryField: 'status', layout: { x: 0, y: 42, w: 3, h: 4 } }, - { id: 'violin', type: 'violin', title: 'Estimate Violin', object: task, aggregate: 'avg', valueField: 'estimate_hours', categoryField: 'priority', layout: { x: 3, y: 42, w: 3, h: 4 } }, - { id: 'candlestick', type: 'candlestick', title: 'Budget Candlestick', object: project, aggregate: 'sum', valueField: 'budget', categoryField: 'start_date', categoryGranularity: 'month', layout: { x: 6, y: 42, w: 3, h: 4 } }, - { id: 'stock', type: 'stock', title: 'Spend Stock', object: project, aggregate: 'sum', valueField: 'spent', categoryField: 'start_date', categoryGranularity: 'month', layout: { x: 9, y: 42, w: 3, h: 4 } }, + // ── Tabular ────────────────────────────────────────────────────────── + { id: 'table_projects', type: 'table', title: 'Projects Table', object: project, aggregate: 'count', layout: { x: 0, y: 30, w: 6, h: 4 } }, + { id: 'pivot_tasks', type: 'pivot', title: 'Tasks Pivot', object: task, aggregate: 'count', categoryField: 'status', layout: { x: 6, y: 30, w: 6, h: 4 } }, ], }; diff --git a/examples/app-showcase/test/coverage.test.ts b/examples/app-showcase/test/coverage.test.ts index b6de66ca3..0197f010a 100644 --- a/examples/app-showcase/test/coverage.test.ts +++ b/examples/app-showcase/test/coverage.test.ts @@ -50,7 +50,7 @@ describe('showcase coverage (introspected against the spec)', () => { it('covers every ChartType', () => { const expected = enumValues(ui.ChartTypeSchema); - expect(expected.length).toBeGreaterThan(30); + expect(expected.length).toBeGreaterThan(20); const used = new Set(); for (const w of ChartGalleryDashboard.widgets ?? []) if (w.type) used.add(w.type); expectFullCoverage('ChartType', expected, used); diff --git a/packages/spec/src/ui/chart.test.ts b/packages/spec/src/ui/chart.test.ts index 878746616..ea4044ec0 100644 --- a/packages/spec/src/ui/chart.test.ts +++ b/packages/spec/src/ui/chart.test.ts @@ -40,8 +40,8 @@ describe('ChartTypeSchema', () => { }); it('should accept all composition chart types', () => { - const types = ['treemap', 'sunburst', 'sankey'] as const; - + const types = ['treemap', 'sankey'] as const; + types.forEach(type => { expect(() => ChartTypeSchema.parse(type)).not.toThrow(); }); @@ -49,25 +49,26 @@ describe('ChartTypeSchema', () => { it('should accept all performance chart types', () => { const types = ['gauge', 'metric', 'kpi'] as const; - + types.forEach(type => { expect(() => ChartTypeSchema.parse(type)).not.toThrow(); }); }); - it('should accept all geo chart types', () => { - const types = ['choropleth', 'bubble-map'] as const; - + it('should accept all advanced chart types', () => { + const types = ['radar'] as const; + types.forEach(type => { expect(() => ChartTypeSchema.parse(type)).not.toThrow(); }); }); - it('should accept all advanced chart types', () => { - const types = ['heatmap', 'radar', 'waterfall', 'box-plot', 'violin'] as const; - - types.forEach(type => { - expect(() => ChartTypeSchema.parse(type)).not.toThrow(); + it('should reject chart types dropped from the taxonomy (unimplementable)', () => { + const removed = ['sunburst', 'word-cloud', 'choropleth', 'bubble-map', 'gl-map', + 'heatmap', 'waterfall', 'box-plot', 'violin', 'candlestick', 'stock'] as const; + + removed.forEach(type => { + expect(() => ChartTypeSchema.parse(type)).toThrow(); }); }); @@ -173,14 +174,14 @@ describe('Real-World Chart Configuration Examples', () => { expect(() => ChartConfigSchema.parse(config)).not.toThrow(); }); - it('should accept heatmap for correlation analysis', () => { + it('should accept treemap for composition analysis', () => { const config: ChartConfig = { - type: 'heatmap', - title: 'User Activity Heatmap', - description: 'Hourly user activity by day of week', + type: 'treemap', + title: 'Hours by Status', + description: 'Relative size of each status bucket', showLegend: true, showDataLabels: false, - colors: ['#440154', '#31688e', '#35b779', '#fde724'], + colors: ['#7C3AED', '#06B6D4', '#10B981', '#F59E0B'], }; expect(() => ChartConfigSchema.parse(config)).not.toThrow(); }); @@ -196,11 +197,11 @@ describe('Real-World Chart Configuration Examples', () => { expect(() => ChartConfigSchema.parse(config)).not.toThrow(); }); - it('should accept waterfall chart for financial analysis', () => { + it('should accept sankey chart for flow analysis', () => { const config: ChartConfig = { - type: 'waterfall', - title: 'Profit & Loss Breakdown', - description: 'Revenue, costs, and profit components', + type: 'sankey', + title: 'Status Flow', + description: 'Flow weighted by record count', showLegend: false, showDataLabels: true, colors: ['#22c55e', '#ef4444', '#6366f1'], diff --git a/packages/spec/src/ui/chart.zod.ts b/packages/spec/src/ui/chart.zod.ts index b557c1db0..580be20b5 100644 --- a/packages/spec/src/ui/chart.zod.ts +++ b/packages/spec/src/ui/chart.zod.ts @@ -43,36 +43,31 @@ export const ChartTypeSchema = lazySchema(() => z.enum([ // Composition 'treemap', - 'sunburst', 'sankey', - 'word-cloud', - + // Performance 'gauge', 'solid-gauge', 'metric', 'kpi', 'bullet', - - // Geo - 'choropleth', - 'bubble-map', - 'gl-map', - + // Advanced - 'heatmap', 'radar', - 'waterfall', - 'box-plot', - 'violin', - 'candlestick', - 'stock', - + // Tabular 'table', 'pivot', ])); +// NOTE: chart families that require data shapes the platform does not model +// (OHLC for candlestick/stock, per-record distributions for box-plot/violin), +// geographic data (choropleth/bubble-map/gl-map), or dependencies the default +// Recharts renderer lacks (sunburst, heatmap, word-cloud, waterfall) were +// removed from this taxonomy: advertising a chart type the renderer can't draw +// is worse than not offering it. They can return via an opt-in renderer plugin +// once there is real demand and a data model to back them. + export type ChartType = z.infer; /** diff --git a/packages/spec/src/ui/dashboard.test.ts b/packages/spec/src/ui/dashboard.test.ts index 7efac76be..fb9adcf58 100644 --- a/packages/spec/src/ui/dashboard.test.ts +++ b/packages/spec/src/ui/dashboard.test.ts @@ -22,7 +22,7 @@ import { ChartTypeSchema } from './chart.zod'; describe('ChartTypeSchema', () => { it('should accept all chart types', () => { - const types = ['metric', 'bar', 'line', 'pie', 'funnel', 'table', 'bubble', 'gauge', 'heatmap', 'pivot', 'grouped-bar']; + const types = ['metric', 'bar', 'line', 'pie', 'funnel', 'table', 'bubble', 'gauge', 'treemap', 'pivot', 'grouped-bar']; types.forEach(type => { expect(() => ChartTypeSchema.parse(type)).not.toThrow(); diff --git a/packages/spec/src/ui/report.test.ts b/packages/spec/src/ui/report.test.ts index 16a3098e9..e1834d727 100644 --- a/packages/spec/src/ui/report.test.ts +++ b/packages/spec/src/ui/report.test.ts @@ -157,7 +157,7 @@ describe('ReportChartSchema', () => { it('should accept different chart types', () => { const types: Array = [ 'bar', 'column', 'line', 'pie', 'donut', 'scatter', 'funnel', - 'area', 'gauge', 'heatmap', 'waterfall', 'metric' + 'area', 'gauge', 'treemap', 'sankey', 'metric' ]; types.forEach(type => {