4.6.2 Reusable token-aware SVG chart primitives in components/ui/ (line/area + grouped bar + axis/grid/legend; a11y data-table fallback) — the viz Story 6.3 reuses
Estimate: 32m · Depends on: 4.6.1
The foundational, reusable chart layer — built once here, consumed by the burndown (4.6.5), the velocity (4.6.6), AND Story 6.3 dashboards ("Charts reuse the viz from Epic 4"). It is design-gated UI, so it carries the 4.6.1 design in dependsOn and is seeded blocked until that lands (Principle #13).
Decision (baked in per the no-shortcut rule): hand-rolled token-aware SVG, not a charting library. The design system routes ALL colour through --el-* and ALL shape through element-semantic tokens (CLAUDE.md); a third-party lib (Recharts / Chart.js / nivo) ships its own styling that bypasses the swap layer, pulls ~80–120 kB for two charts, and fights the a11y model. Two small charts do not earn that. So this subtask builds lightweight SVG primitives that consume the design tokens directly — the durable shape, and the one Epic 6.3 extends.
Build (in components/ui/charts/ or components/ui/):
- A shared chart frame — responsive SVG viewBox, the X/Y axes + tick labels, gridlines, and a margin/scale helper (a tiny linear-scale util; do NOT add d3). Colour via
--el-*(the series tokens 4.6.1 specifies), shape via element shape tokens. - A
LineChart/ area primitive — multiple series, including a STEP-interpolated series (for the burndown actual) and a straight guideline series, with optional point markers (the scope-change markers) and a "now" reference line. - A grouped
BarChartprimitive — N categories × 2 bars (committed/completed) + an optional average reference line. - A
ChartLegend+ the a11y fallback: every chart renders a visible text legend,role="img"+aria-label/aria-describedbysummarising the series, and a visually-hidden (or toggle-revealed)<table>of the underlying data — so the chart is conveyed as text+number, never colour/shape alone (finding #35). Charts take data as plain typed props (series arrays + labels); they are PURE presentational components (no fetching, no service imports), unit-testable in isolation and previewable on a specimen//tokens-style page.
Acceptance criteria
components/uiexports aLineChart(multi-series, supports a step series + a straight guideline + a reference line + point markers) and a groupedBarChart(paired bars + average reference), plus a shared chart frame (axes/ticks/gridlines) andChartLegend, matchingdesign/reports/charts.mock.html.- Colour comes ONLY from
--el-*(the 4.6.1 series tokens, added to globals.css Tier 3 if missing); shape from element shape tokens; no Tier-0--color-*, no rawrounded-*/p-*for surface shape (motir-core/CLAUDE.md). - Each chart is a11y-complete: a visible text legend + axis labels,
role="img"with anaria-label/aria-describedbysummary, and a data-table fallback conveying every series as text+number (finding #35); colour/shape never the sole signal. - The charts are PURE components (typed data props in, SVG out — no data fetching, no service imports); component tests render each with sample data + assert the a11y fallback table; no charting library is added to
package.json. - The primitive is generic enough for Story 6.3 to reuse (documented in
design-notes.md/ a short component doc) — not hard-coded to sprints.
Context refs
design/reports/charts.mock.html+design/reports/design-notes.md(4.6.1) — the chart visual language + the--el-*series tokens to consumecomponents/ui/*(the existing primitive conventions — props/variants/exports) + the/tokensspecimen route pattern (1.0.5) to add a chart specimenapp/globals.cssTier-3--el-*+ the element-shape tokens;motir-core/CLAUDE.md(colour via--el-*, shape via element tokens)- Story 6.3 (dashboards & reports) — the downstream consumer to keep the API generic for; findings #35 (read as text), #54 (use the palette)