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
12 changes: 12 additions & 0 deletions .changeset/chart-gallery-renderable-types.md
Original file line numberDiff line numberDiff line change
@@ -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).
2 changes: 1 addition & 1 deletion .objectui-sha
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
d2972a54a31cb26dc22627faba333a7fb6839101
76094e0d4ed79b8732ba9879c75aa5ab8283180d
86 changes: 36 additions & 50 deletions examples/app-showcase/src/dashboards/chart-gallery.dashboard.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 } },
],
};
18 changes: 15 additions & 3 deletions examples/app-showcase/test/coverage.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string>();
for (const w of ChartGalleryDashboard.widgets ?? []) if (w.type) used.add(w.type);
expectFullCoverage('ChartType', expected, used);
Expand Down
Loading