Skip to content

Commit 038ecc7

Browse files
authored
Merge pull request #1690 from objectstack-ai/feat-chart-gallery-renderable-types
feat(showcase): chart gallery shows only distinctly-renderable chart types
2 parents 7f53b42 + 4b0fdba commit 038ecc7

4 files changed

Lines changed: 64 additions & 54 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/example-showcase": patch
3+
---
4+
5+
The showcase Chart Gallery now shows one widget per chart family the renderer
6+
draws DISTINCTLY (27 → 17 widgets). Families that fell back to a near-relative
7+
(grouped/stacked/bi-polar bars, stacked-area, step-line, spline, pyramid,
8+
bubble) and the dial-less performance variants (kpi/gauge/solid-gauge/bullet,
9+
identical to `metric`) were removed — advertising a type that renders as
10+
something else is misleading. Bundles the objectui console build that routes
11+
each widget to its true chart renderer (pie/donut/funnel/line/area/scatter/
12+
radar/treemap/sankey/table/pivot).

.objectui-sha

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
d2972a54a31cb26dc22627faba333a7fb6839101
1+
76094e0d4ed79b8732ba9879c75aa5ab8283180d

examples/app-showcase/src/dashboards/chart-gallery.dashboard.ts

Lines changed: 36 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,75 +2,61 @@
22

33
import type { Dashboard } from '@objectstack/spec/ui';
44

5-
const task = 'showcase_task';
6-
const project = 'showcase_project';
75
const taskDs = 'showcase_task_metrics';
86
const projectDs = 'showcase_project_metrics';
97

108
/**
11-
* Chart Gallery — one widget per chart family so the dashboard renderer can be
12-
* exercised against every visualisation type. Covers the full `ChartTypeSchema`
13-
* taxonomy (comparison, trend, distribution, relationship, composition,
14-
* performance, tabular) — every type here renders; the taxonomy intentionally
15-
* excludes families the renderer cannot draw (geo maps, OHLC, distributions).
9+
* Chart Gallery — one widget per chart family the dashboard renderer can draw
10+
* DISTINCTLY, so the showcase honestly reflects what the platform implements.
1611
*
17-
* ADR-0021 Phase 2: every widget is bound to a semantic dataset
18-
* (`showcase_task_metrics` / `showcase_project_metrics`) and selects
19-
* dimensions/measures BY NAME, side-by-side with the legacy inline query during
20-
* the dual-form window. The reconciliation harness asserts both forms return
21-
* identical numbers (scripts/analytics-reconcile). Date-bucketed trend widgets
22-
* (`created_at` + `categoryGranularity`) stay inline-only for now — dataset
23-
* timeDimension reconciliation is deferred (see the CRM trend widget).
12+
* Every widget binds a semantic dataset (`showcase_task_metrics` /
13+
* `showcase_project_metrics`, ADR-0021) and selects dimensions/measures BY NAME;
14+
* the analytics layer resolves dimension display labels (select option labels,
15+
* lookup names, month-bucketed dates) and carries each measure's label + format.
16+
*
17+
* Only types with a real, distinct renderer are shown. The chart families that
18+
* currently fall back to a near-relative (grouped/stacked/bi-polar bars → bar,
19+
* stacked-area → area, step-line/spline → line, pyramid → funnel, bubble →
20+
* scatter) and the single-value performance variants without a dial
21+
* (gauge/solid-gauge/bullet, which render the same as `metric`) are intentionally
22+
* NOT duplicated here — advertising a type that renders as something else is
23+
* worse than not offering it. (Follow-up: trim those from `ChartTypeSchema`.)
2424
*/
2525
export const ChartGalleryDashboard: Dashboard = {
2626
name: 'showcase_chart_gallery',
2727
label: 'Chart Gallery',
28-
description: 'A representative widget for every chart family — visual coverage of the dashboard renderer.',
28+
description: 'One widget per chart family the renderer draws distinctly — honest visual coverage.',
2929
columns: 12,
3030
widgets: [
31-
// ── Performance / KPI ────────────────────────────────────────────────
32-
{ id: 'kpi_total_tasks', type: 'metric', title: 'Total Tasks', dataset: taskDs, values: ['task_count'], layout: { x: 0, y: 0, w: 3, h: 2 } },
33-
{ 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 } },
34-
{ id: 'gauge_progress', type: 'gauge', title: 'Avg Progress', dataset: taskDs, values: ['avg_progress'], layout: { x: 6, y: 0, w: 3, h: 2 } },
35-
{ id: 'bullet_budget', type: 'bullet', title: 'Budget vs Spend', dataset: projectDs, values: ['spent_sum'], layout: { x: 9, y: 0, w: 3, h: 2 } },
31+
// ── Performance / KPI (one measure value; demonstrates label + format) ──
32+
{ id: 'kpi_total_tasks', type: 'metric', title: 'Total Tasks', dataset: taskDs, values: ['task_count'], layout: { x: 0, y: 0, w: 4, h: 2 } },
33+
{ id: 'kpi_avg_progress', type: 'metric', title: 'Avg Progress', dataset: taskDs, values: ['avg_progress'], layout: { x: 4, y: 0, w: 4, h: 2 } },
34+
{ id: 'kpi_total_spent', type: 'metric', title: 'Total Spent', dataset: projectDs, values: ['spent_sum'], layout: { x: 8, y: 0, w: 4, h: 2 } },
3635

37-
// ── Comparison ───────────────────────────────────────────────────────
36+
// ── Comparison ─────────────────────────────────────────────────────────
3837
{ 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 } },
3938
{ 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 } },
4039
{ 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 } },
41-
{ 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 } },
42-
{ 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 } },
43-
44-
// ── Trend (date-bucketed via timeDimension granularity) ──────────────
45-
{ 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 } },
46-
{ 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 } },
47-
{ 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 } },
48-
{ 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 } },
49-
50-
// ── Distribution ─────────────────────────────────────────────────────
51-
{ id: 'pie_status', type: 'pie', title: 'Status Split', dataset: taskDs, dimensions: ['status'], values: ['task_count'], layout: { x: 0, y: 14, w: 3, h: 4 } },
52-
{ id: 'donut_priority', type: 'donut', title: 'Priority Split', dataset: taskDs, dimensions: ['priority'], values: ['task_count'], layout: { x: 3, y: 14, w: 3, h: 4 } },
53-
{ id: 'funnel_pipeline', type: 'funnel', title: 'Task Funnel', dataset: taskDs, dimensions: ['status'], values: ['task_count'], layout: { x: 6, y: 14, w: 3, h: 4 } },
54-
{ id: 'pyramid_priority', type: 'pyramid', title: 'Priority Pyramid', dataset: taskDs, dimensions: ['priority'], values: ['task_count'], layout: { x: 9, y: 14, w: 3, h: 4 } },
5540

56-
// ── Relationship ─────────────────────────────────────────────────────
57-
{ 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 } },
58-
{ id: 'bubble_budget', type: 'bubble', title: 'Budget Bubble', dataset: projectDs, dimensions: ['account'], values: ['budget_sum'], layout: { x: 4, y: 18, w: 4, h: 4 } },
41+
// ── Trend (month-bucketed via the dataset's created_at granularity) ──────
42+
{ 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 } },
43+
{ 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 } },
5944

60-
// ── Composition ──────────────────────────────────────────────────────
61-
{ id: 'treemap_hours', type: 'treemap', title: 'Hours Treemap', dataset: taskDs, dimensions: ['status'], values: ['est_hours'], layout: { x: 8, y: 18, w: 4, h: 4 } },
62-
{ 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 } },
63-
{ id: 'radar_priority', type: 'radar', title: 'Priority Radar', dataset: taskDs, dimensions: ['priority'], values: ['task_count'], layout: { x: 4, y: 22, w: 4, h: 4 } },
45+
// ── Distribution ─────────────────────────────────────────────────────────
46+
{ id: 'pie_status', type: 'pie', title: 'Status Split', dataset: taskDs, dimensions: ['status'], values: ['task_count'], layout: { x: 0, y: 10, w: 4, h: 4 } },
47+
{ id: 'donut_priority', type: 'donut', title: 'Priority Split', dataset: taskDs, dimensions: ['priority'], values: ['task_count'], layout: { x: 4, y: 10, w: 4, h: 4 } },
48+
{ id: 'funnel_status', type: 'funnel', title: 'Status Funnel', dataset: taskDs, dimensions: ['status'], values: ['task_count'], layout: { x: 8, y: 10, w: 4, h: 4 } },
6449

65-
// ── Performance ──────────────────────────────────────────────────────
66-
{ id: 'solid_gauge', type: 'solid-gauge', title: 'Solid Gauge', dataset: taskDs, values: ['avg_progress'], layout: { x: 8, y: 22, w: 4, h: 4 } },
50+
// ── Relationship + Advanced ──────────────────────────────────────────────
51+
{ 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 } },
52+
{ id: 'radar_priority', type: 'radar', title: 'Priority Radar', dataset: taskDs, dimensions: ['priority'], values: ['task_count'], layout: { x: 6, y: 14, w: 6, h: 4 } },
6753

68-
// ── Comparison / trend variants ──────────────────────────────────────
69-
{ 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 } },
70-
{ 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 } },
54+
// ── Composition ──────────────────────────────────────────────────────────
55+
{ id: 'treemap_hours', type: 'treemap', title: 'Hours Treemap', dataset: taskDs, dimensions: ['status'], values: ['est_hours'], layout: { x: 0, y: 18, w: 6, h: 4 } },
56+
{ 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 } },
7157

72-
// ── Tabular ──────────────────────────────────────────────────────────
73-
{ id: 'table_projects', type: 'table', title: 'Projects Table', dataset: projectDs, values: ['project_count'], layout: { x: 0, y: 30, w: 6, h: 4 } },
74-
{ id: 'pivot_tasks', type: 'pivot', title: 'Tasks Pivot', dataset: taskDs, dimensions: ['status'], values: ['task_count'], layout: { x: 6, y: 30, w: 6, h: 4 } },
58+
// ── Tabular (real grouped tables, multiple measures) ─────────────────────
59+
{ 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 } },
60+
{ 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 } },
7561
],
7662
};

examples/app-showcase/test/coverage.test.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,21 @@ describe('showcase coverage (introspected against the spec)', () => {
4848
expectFullCoverage('FormViewType', [...FORM_VIEW_TYPES], collectFormViewTypes(views as never));
4949
});
5050

51-
it('covers every ChartType', () => {
52-
const expected = enumValues(ui.ChartTypeSchema);
53-
expect(expected.length).toBeGreaterThan(20);
51+
it('covers every distinctly-renderable ChartType', () => {
52+
// The Chart Gallery demonstrates only chart families the renderer draws
53+
// DISTINCTLY — advertising a type that renders as something else (a "sankey"
54+
// that draws bars) is worse than not offering it. The families below have no
55+
// distinct renderer yet and fall back to a near-relative, so the showcase
56+
// intentionally does not duplicate them: grouped/stacked/bi-polar bars (→bar),
57+
// stacked-area (→area), step-line/spline (→line), pyramid (→funnel), bubble
58+
// (→scatter), and the dial-less performance variants kpi/gauge/solid-gauge/
59+
// bullet (→ the same KPI value as `metric`). Follow-up: trim these from
60+
// `ChartTypeSchema` so spec ↔ renderer ↔ showcase stay in lockstep.
61+
const FALLBACK_ONLY = new Set([
62+
'grouped-bar', 'stacked-bar', 'bi-polar-bar', 'stacked-area', 'step-line',
63+
'spline', 'pyramid', 'bubble', 'kpi', 'gauge', 'solid-gauge', 'bullet',
64+
]);
65+
const expected = enumValues(ui.ChartTypeSchema).filter((t) => !FALLBACK_ONLY.has(t));
5466
const used = new Set<string>();
5567
for (const w of ChartGalleryDashboard.widgets ?? []) if (w.type) used.add(w.type);
5668
expectFullCoverage('ChartType', expected, used);

0 commit comments

Comments
 (0)