Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .objectui-sha
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3442c5547328cb5bbbd7ef8a2e704b0d4f0268f3
5265dd315b6b5e1661c7d6da67bfd4759b2f0dee
43 changes: 17 additions & 26 deletions examples/app-showcase/src/dashboards/chart-gallery.dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 } },
],
};
2 changes: 1 addition & 1 deletion examples/app-showcase/test/coverage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>();
for (const w of ChartGalleryDashboard.widgets ?? []) if (w.type) used.add(w.type);
expectFullCoverage('ChartType', expected, used);
Expand Down
41 changes: 21 additions & 20 deletions packages/spec/src/ui/chart.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,34 +40,35 @@ 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();
});
});

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();
});
});

Expand Down Expand Up @@ -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();
});
Expand All @@ -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'],
Expand Down
27 changes: 11 additions & 16 deletions packages/spec/src/ui/chart.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof ChartTypeSchema>;

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/src/ui/dashboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/src/ui/report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ describe('ReportChartSchema', () => {
it('should accept different chart types', () => {
const types: Array<ReportChart['type']> = [
'bar', 'column', 'line', 'pie', 'donut', 'scatter', 'funnel',
'area', 'gauge', 'heatmap', 'waterfall', 'metric'
'area', 'gauge', 'treemap', 'sankey', 'metric'
];

types.forEach(type => {
Expand Down