Chart
A polymorphic data-visualisation primitive. One namespace, twenty sub-types — line, bar, donut, sparkline, heatmap, funnel, scatter, radar, treemap, gauge, waterfall, candlestick, and friends — that share a single data, axes and accessibility model.
Overview
Chart is a namespace, not a single component. Chart.Line, Chart.Bar, Chart.Sparkline and seventeen others all share the same data props, the same accessibility contract, and the same theming hooks.
The system's stance is opinionated: charts are not decoration, they answer a question. Every Chart.* sub-type is built around a question — "how is this trending?" (line), "how big is each bucket?" (bar), "what's the composition?" (donut, treemap), "did we hit target?" (bullet, gauge), "what's the bridge?" (waterfall). The sub-type is the verb; the data is the subject; the props are the adverbs.
Chart when you have four or more data points and the shape of the data carries information the user couldn't see by reading the numbers. Don't chart below four points — a stat tile or two number deltas tells the same story with less ink. Never chart a single number — that's a stat hero's job.
Parts · 20 sub-types
Each tile links to its cookbook recipe — chart in context, customisation diff, accessibility notes, the lot.
Common props
Every Chart.* shares these props, even when the sub-type adds its own (e.g. regression on Chart.Scatter, bands on Chart.Bullet).
| Prop | Type | Default | Notes |
|---|---|---|---|
data | T[] | — | The series. Shape depends on sub-type — number[] for sparkline, {date,value}[] for line, {x,y}[] for scatter. |
xAccessor / yAccessor | keyof T | (d) => number | 'x' / 'y' | How to pull x and y from each datum. Pass a function for derived values. |
color / tone | 'brand' | 'success' | 'danger' | 'neutral' | string | 'brand' | Picks from token palette. Pass a token name; raw hex codes are rejected at type-check. |
width / height | number | responsive | Pass numbers for fixed; omit to fill the parent with preserveAspectRatio. |
legend | boolean | 'inline' | 'side' | false | Multi-series charts auto-enable; single-series stays off. |
axes | boolean | { x?: AxisProps; y?: AxisProps } | true | Sub-types like sparkline, donut and gauge hide axes by default. |
grid | boolean | 'x' | 'y' | 'both' | 'y' | Horizontal gridlines by default; pass false to drop them. |
tooltip | boolean | (d) => ReactNode | true | Hover/keyboard tooltip. Pass a function for custom content. |
loading | boolean | false | Renders an animated skeleton in the chart's shape. |
empty | ReactNode | default empty state | Shown when data.length === 0. |
format | (v: number) => string | locale-default | How values are formatted in labels, tooltips, and the fallback table. |
aria-label | string | — | Required. A sentence describing what the chart shows and the headline insight. |
description | string | — | Long-form description for screen readers; lands in the SVG <desc>. |
API shape (TypeScript)
The intended exports as of v0.2. Every sub-type narrows the base ChartProps<T> with its own required and optional fields.
// Base — shared by every Chart.* sub-type. type ChartProps<T> = { data: T[]; xAccessor?: keyof T | ((d: T) => number | string); yAccessor?: keyof T | ((d: T) => number); color?: 'brand' | 'success' | 'danger' | 'neutral'; width?: number; height?: number; legend?: boolean | 'inline' | 'side'; axes?: boolean | { x?: AxisProps; y?: AxisProps }; tooltip?: boolean | ((d: T) => React.ReactNode); loading?: boolean; empty?: React.ReactNode; format?: (v: number) => string; 'aria-label': string; // required description?: string; }; // Sub-type signatures (selected). type LineProps<T> = ChartProps<T> & { smooth?: boolean; area?: boolean; markers?: 'none' | 'endpoints' | 'all'; }; type BulletProps = { label: string; value: number; target: number; max?: number; bands?: Array<{ to: number; tone: Tone; label?: string }>; orientation?: 'horizontal' | 'vertical'; 'aria-label': string; }; type SparklineProps = { data: number[]; tone?: Tone; variant?: 'line' | 'bar' | 'winloss'; area?: boolean; endpoint?: boolean; markers?: 'none' | 'extrema'; width?: number; height?: number; 'aria-label': string; }; type FunnelStep = { label: string; value: number }; type FunnelProps = { steps: FunnelStep[]; showRates?: boolean; highlightWorstStep?: boolean; orientation?: 'vertical' | 'horizontal'; 'aria-label': string; }; // The namespace. export declare namespace Chart { const Line: <T>(p: LineProps<T>) => JSX.Element; const Area: <T>(p: AreaProps<T>) => JSX.Element; const Bar: <T>(p: BarProps<T>) => JSX.Element; const Donut: (p: DonutProps) => JSX.Element; const Pie: (p: PieProps) => JSX.Element; const ProgressRing: (p: ProgressRingProps) => JSX.Element; const Sparkline: (p: SparklineProps) => JSX.Element; const Bullet: (p: BulletProps) => JSX.Element; const Heatmap: (p: HeatmapProps) => JSX.Element; const Funnel: (p: FunnelProps) => JSX.Element; const Scatter: <T>(p: ScatterProps<T>) => JSX.Element; const Radar: (p: RadarProps) => JSX.Element; const Treemap: (p: TreemapProps) => JSX.Element; const Gauge: (p: GaugeProps) => JSX.Element; const Waterfall: (p: WaterfallProps) => JSX.Element; const Candlestick: (p: CandlestickProps) => JSX.Element; }
Accessibility model
Every chart in the system follows the same contract. The renderer emits the structure; you supply the labels.
- Every chart is
role="img"on the SVG root. Charts are not interactive widgets — they're images of data. Screen readers treat them as a single unit; they don't try to recite every<rect>. - The
aria-labelis required. The compiler rejects aChart.*without one. The label must be a sentence: subject, headline insight, and the most surprising data point. "Revenue over 14 days, up 18%, biggest jump on day 12" — not "Line chart". - Long descriptions go in SVG
<title>+<desc>. Thedescriptionprop lands in<desc>; the chart name lands in<title>. Screen readers read both. - A hidden table fallback ships by default. Every chart emits an
aria-hidden="false"<table>insr-onlystyles, with one row per data point. Override withfallback={false}only if the chart is purely decorative — and then passaria-hidden="true"too. - Keyboard navigation per data point. Tab into the chart, arrow keys move between adjacent points, Enter triggers any
onPointClick. The currently-focused point announces its value. - Direction never colour-only. Up/down/loss/gain is always backed by text — delta badges, sign prefixes, or icons. Colour does emotion; text does the data.
- Tooltips are pointer-and-keyboard. Charts never put critical data in a hover-only tooltip; touch and keyboard users can always reach it via focus or the fallback table.
- Reduced motion is honoured. Animated reveals (draw-in, bar grow) are skipped when
prefers-reduced-motion: reduceis set; the final state renders immediately.
Do & don't
Pick the sub-type by the question. "How is it trending?" → Line. "How big is each?" → Bar. "Where did the money go?" → Treemap or Waterfall.
Reach for the chart that looks impressive. A radar where bars would work is showing off, not answering.
Pair every chart with one short caption — the headline insight. "Revenue up 18% — biggest jump on day 12."
Make the user reverse-engineer the chart. If the insight isn't above the chart, no one will find it below it.
Encode direction with both colour and text — green and "+18%", red and "−6%". Colour is the verdict, text is the fact.
Use colour as the only carrier of meaning. Colour-blind users, monochrome screens, and dark mode will all betray you.
Chart at four points and up. Below that, render the numbers — a stat with a delta tells the same story with less ink.
Build a one-bar bar chart or a two-slice donut. They're worse than the figures they encode.
All 20 recipes
Every sub-type has a recipe in the cookbook — the question it answers, when not to use it, and a 6-line customisation diff.
Related
For composing charts into a dashboard, see the Stat hero block and the Executive dashboard pattern. The charts hub at Cookbook · Charts is the front door for every sub-type.