Chart
A chart figure built with Recharts: bars, columns, lines and stacked areas in the chart's two scales plus patterns, with direct labels, a restyled tooltip and a data table.
import { Chart } from '@fairgarden-private/design/components/Chart'
Every fill, stroke and label is passed to Recharts as a role variable (var(--secondary11), var(--primary12), var(--role-rule)), so the chart follows the mode with no script and prints black through the remap. Series are told apart by the pattern-series slots, on screen as in print: slot 1 solid, then hatch 45°, dot screen, hatch 0°, crossed hatch, and slot 6 outlined ("Other"). Odd slots take --secondary11, even slots --primary12, and every shape has a --border-size-1 --primary12 edge. Each chart defines its patterns in its own SVG defs, with ids unique to the instance. Fills are opaque, with no gradients, shadows or fading. Recharts' accessibility layer is on: focus the chart and use the arrow keys to move the tooltip across the categories.
Horizontal bars
Horizontal bars
kind="bar" (default) is the form preferred on mobile: labels on the left, 16 px bars from a shared zero line, and each value at its bar's end in type-data. Two or more grouped series need the custom legend, which shows 16 px swatches of the fills actually drawn. The caption is the chart's title and says what to notice; the source line follows it.
- Spring
- Fall
Volunteer hours
'use client'
import * as React from 'react'
import { Chart } from '@fairgarden-private/design/components/Chart'
const hours = [
{ garden: 'Parkside', spring: 412, fall: 356 },
{ garden: 'Riverbend', spring: 298, fall: 331 },
{ garden: 'Hillcrest', spring: 241, fall: 187 },
{ garden: 'Orchard Row', spring: 176, fall: 204 },
{ garden: 'Mill Pond', spring: 94, fall: 118 },
]
export function ChartBars() {
return (
<Chart
data={hours}
categoryKey="garden"
categoryLabel="Garden"
series={[
{ key: 'spring', name: 'Spring' },
{ key: 'fall', name: 'Fall' },
]}
valueLabel="Volunteer hours"
figureLabel="Fig. 1"
caption="Parkside logged the most volunteer hours in both seasons."
source="Source: FairGarden sign-in sheets, 2026."
/>
)
}
Columns
Columns from 1024 px
kind="column" draws horizontal bars until its container is 1024 px wide, then columns; print always uses bars, because the printed column is narrower. The switch is a container rule on the chart's wrapper, not a width check in script. Gridlines are relations, so they are line-dotted-fine in --role-rule; the zero line stays solid.
Visitors
'use client'
import * as React from 'react'
import { Chart } from '@fairgarden-private/design/components/Chart'
const visitors = [
{ month: 'Apr', visitors: 1840 },
{ month: 'May', visitors: 2960 },
{ month: 'Jun', visitors: 3410 },
{ month: 'Jul', visitors: 3125 },
{ month: 'Aug', visitors: 2780 },
{ month: 'Sep', visitors: 2215 },
]
export function ChartColumns() {
return (
<Chart
kind="column"
data={visitors}
categoryKey="month"
categoryLabel="Month"
series={[{ key: 'visitors', name: 'Visitors' }]}
valueLabel="Visitors"
caption="Visits peaked in June and eased through the late summer."
source="Source: Gate counters, April to September 2026."
/>
)
}
Lines
Lines and markers
Lines are --border-size-2, each in its slot's ink, and are told apart by point markers (● ○ ■ □ ▲), never by dashes, because dashes mean a path. Each line ends in a direct label with the series name and its last value. The cursor is a vertical line-dotted-fine in --primary12.
Birds counted
'use client'
import * as React from 'react'
import { Chart } from '@fairgarden-private/design/components/Chart'
const counts = [
{ year: '2020', warblers: 38, sparrows: 64, finches: 22 },
{ year: '2021', warblers: 44, sparrows: 61, finches: 27 },
{ year: '2022', warblers: 51, sparrows: 58, finches: 31 },
{ year: '2023', warblers: 57, sparrows: 60, finches: 29 },
{ year: '2024', warblers: 66, sparrows: 55, finches: 35 },
{ year: '2025', warblers: 71, sparrows: 53, finches: 38 },
]
export function ChartLines() {
return (
<Chart
kind="line"
data={counts}
categoryKey="year"
categoryLabel="Year"
series={[
{ key: 'warblers', name: 'Warblers' },
{ key: 'sparrows', name: 'Sparrows' },
{ key: 'finches', name: 'Finches' },
]}
valueLabel="Birds counted"
caption="Warblers overtook sparrows in 2024 as the hedgerows matured."
source="Source: Spring bird count, one morning each May."
/>
)
}
Stacked areas
Stacked areas
With more than one series, kind="area" always stacks, because two patterns never overlap. Each band is a slot fill edged in --primary12 and named inside itself at the last point, on a --role-halo knockout. The remainder takes slot 6.
Compost collected (kg)
'use client'
import * as React from 'react'
import { Chart } from '@fairgarden-private/design/components/Chart'
const compost = [
{ month: 'May', kitchen: 120, yard: 210, other: 40 },
{ month: 'Jun', kitchen: 135, yard: 260, other: 45 },
{ month: 'Jul', kitchen: 150, yard: 240, other: 55 },
{ month: 'Aug', kitchen: 160, yard: 220, other: 50 },
{ month: 'Sep', kitchen: 170, yard: 300, other: 60 },
{ month: 'Oct', kitchen: 175, yard: 380, other: 70 },
]
export function ChartAreas() {
return (
<Chart
kind="area"
data={compost}
categoryKey="month"
categoryLabel="Month"
series={[
{ key: 'kitchen', name: 'Kitchen scraps' },
{ key: 'yard', name: 'Yard waste' },
{ key: 'other', name: 'Other', slot: 6 },
]}
valueLabel="Compost collected (kg)"
formatValue={(value) => `${value.toLocaleString('en-US')} kg`}
caption="Yard waste drives the autumn rise; kitchen scraps grow steadily."
/>
)
}
Sequential steps
Sequential steps
steps encodes a 1–5 rating on a single-series bar or column chart. On screen each bar takes its step's fill (--secondary4, 6, 8, 11, 12); in print, and wherever the chart is one ink, it shows that step's pattern instead: empty, dot screens at 6 and 4 px, the heavy dot screen, then crossed hatch. Each bar carries both layers, and the print block hides the flat fill. The legend is a framed stepped bar with end labels.
Households waiting for a plot
'use client'
import * as React from 'react'
import { Chart } from '@fairgarden-private/design/components/Chart'
const plots = [
{ garden: 'Parkside', waiting: 46, demand: 5 },
{ garden: 'Riverbend', waiting: 31, demand: 4 },
{ garden: 'Hillcrest', waiting: 18, demand: 3 },
{ garden: 'Orchard Row', waiting: 9, demand: 2 },
{ garden: 'Mill Pond', waiting: 3, demand: 1 },
]
export function ChartSteps() {
return (
<Chart
data={plots}
categoryKey="garden"
categoryLabel="Garden"
series={[{ key: 'waiting', name: 'Households waiting' }]}
steps={{ key: 'demand', name: 'Demand', labels: ['1/5 low', '5/5 high'] }}
valueLabel="Households waiting for a plot"
caption="The longest waiting lists sit where demand is rated highest."
/>
)
}
Single highlight and one ink
Single highlight and one ink
highlight gives one series slot 1 and outlines the rest, the preferred form for one against the rest. The ground picks the inks: on a light base ground (paper, white) or a nested white plate the odd slots take --secondary11 and step bars their flat fills; anywhere else, the pastels included, every slot turns --primary12 and step bars show their patterns, while the patterns keep the series apart. oneInk forces one ink on a light base ground too.
- 2024
- 2025
- 2026
Harvest (kg)
- 2024
- 2025
- 2026
Harvest (kg)
'use client'
import * as React from 'react'
import { Chart } from '@fairgarden-private/design/components/Chart'
import styles from './highlight.module.css'
const harvest = [
{ crop: 'Tomatoes', y2024: 310, y2025: 342, y2026: 398 },
{ crop: 'Beans', y2024: 180, y2025: 171, y2026: 226 },
{ crop: 'Squash', y2024: 142, y2025: 160, y2026: 151 },
]
const series = [
{ key: 'y2024', name: '2024' },
{ key: 'y2025', name: '2025' },
{ key: 'y2026', name: '2026' },
] as const
export function ChartHighlight() {
return (
<div className={styles.stack}>
<Chart
data={harvest}
categoryKey="crop"
categoryLabel="Crop"
series={series}
highlight="y2026"
valueLabel="Harvest (kg)"
caption="2026 against the two years before it: one series solid, the rest outlined."
/>
<Chart
data={harvest}
categoryKey="crop"
categoryLabel="Crop"
series={series}
oneInk
valueLabel="Harvest (kg)"
caption="The same data in one ink, as on a deep or saturated ground: patterns carry the series."
/>
</div>
)
}
Data table, motion and print
Every chart ends with a "Show Data Table" Collapsible holding a §8.2 table with the same series names and units; the tooltip never shows a value the table lacks. Animation is off unless the reader's motion preference allows it, and then bars grow and lines draw by length, never by opacity. On screen the chart fills its container's width; in print it is drawn at a fixed 174 mm (--ds-print-live-width) with animation off, and a chart whose values lived only in the tooltip (lines, areas, charts without labels) prints its data table too.
API Reference
A chart figure (§8.7) built with Recharts [D167]. Categories are told
apart by the pattern-series slots, on screen as in print: odd slots in
--secondary11, even in --primary12, slot 1 solid, slot 6 outline, every
shape edged in --border-size-1 --primary12 [D125]. Values are labelled
directly; the tooltip and the keyboard accessibility layer repeat them;
the “Show Data Table” Collapsible holds them all. The chart follows its
container’s width on screen and prints at a fixed 174 mm, animation off.
| Prop | Type | Description |
|---|---|---|
caption | | The figure caption, which is the chart’s title: say what to notice. |
categoryKey | | The row field naming each category (bar label, column or time tick). |
categoryLabel | | Head of the category column in the data table. Default “Category”. |
data | | The rows, one per category, in display order. |
figureLabel | | The figure label, e.g. “Fig. 3″, on its own line above the caption. |
formatValue | | Formats every value: labels, ticks, tooltip and table. Default: grouped
digits ( |
height | | Plot height in px on screen. Default: bars from their rows (16 px bars), other charts 320. |
highlight | | Single highlight: this series takes slot 1 and every other series slot 6 (outline), the preferred form for “one against the rest”. |
kind | |
|
labels | | Direct labels ( |
legend | | The custom legend above the chart (beside it from 1024 px), with 16 px swatches showing the fill drawn. Default: shown for grouped or stacked bars with more than one series, where direct labels cannot name them. |
oneInk | | Forces one ink: every slot in --primary12 and sequential cells as
patterns. Not needed for the ground: the scope already draws one ink
everywhere but a light base ( |
primary | | Primary Radix scale: even slots, slot 6, edges, axes and labels. Never defaulted; omitted, it inherits the scope [D133]. |
printHeight | | Plot height in px in print. Default: the screen height. |
secondary | | Secondary Radix scale: odd slots and sequential steps. Keep step 11 at 3:1 on the ground (§2 pairing matrix) [D128]. Never defaulted. |
series | | The series, in the semantic order: at most 4 (6 at the hard limit; beyond that, small multiples). Extra series are not drawn. |
source | | Source and method notes, e.g. “Source: Trail counters, 2026.” |
stacked | | Stacks bar and column series into one bar per category. Area series always stack. |
steps | | A sequential encoding for a single-series bar or column chart: each bar takes its row’s step fill (secondary 4 / 6 / 8 / 11 / 12 on screen), and its print pattern as a print-only layer. |
tableLabel | | The data-table trigger label. Default “Show Data Table”. |
tableOpenLabel | | The trigger label while the table is open. Default “Hide Data Table”. |
valueLabel | | Value axis title with its units, e.g. “Visitors (thousands)". |
Additional types
ChartKind
The chart’s structural build.
type ChartKind = 'area' | 'line' | 'bar' | 'column'<'area' | 'line' | 'bar' | 'column' | null | undefined>ChartProps
Props for Chart: div props plus the data, the encoding and the figure text.
type ChartProps<Row extends {} = Record<string, unknown>> = {
/**
* `bar` (default): horizontal bars, labels left, values at the bar ends,
* the form preferred on mobile. `column`: horizontal bars below 1024 px of
* the container and in print, columns from 1024 px. `line`: lines told
* apart by point markers (● ○ ■ □ ▲), never by dashes. `area`: stacked
* areas; several series always stack, because patterns never overlap.
*/
kind?: 'area' | 'line' | 'bar' | 'column' | null;
/** The rows, one per category, in display order. */
data: Row[];
/** The row field naming each category (bar label, column or time tick). */
categoryKey: (string & string) | number | symbol;
/** Head of the category column in the data table. Default "Category". */
categoryLabel?: string;
/**
* The series, in the semantic order: at most 4 (6 at the hard limit;
* beyond that, small multiples). Extra series are not drawn.
*/
series: ChartSeries<(string & string) | number | symbol>[];
/** The figure caption, which is the chart's title: say what to notice. */
caption: React.ReactNode;
/** The figure label, e.g. "Fig. 3", on its own line above the caption. */
figureLabel?: React.ReactNode;
/** Source and method notes, e.g. "Source: Trail counters, 2026." */
source?: React.ReactNode;
/** Value axis title with its units, e.g. "Visitors (thousands)". */
valueLabel?: string;
/**
* Formats every value: labels, ticks, tooltip and table. Default: grouped
* digits (`toLocaleString('en-US')`).
*/
formatValue?: (value: number) => string;
/** Stacks bar and column series into one bar per category. Area series always stack. */
stacked?: boolean;
/**
* Single highlight: this series takes slot 1 and every other series
* slot 6 (outline), the preferred form for "one against the rest".
*/
highlight?: (string & string) | number | symbol;
/**
* A sequential encoding for a single-series bar or column chart: each bar
* takes its row's step fill (secondary 4 / 6 / 8 / 11 / 12 on screen), and
* its print pattern as a print-only layer.
*/
steps?: ChartSteps<(string & string) | number | symbol>;
/**
* Direct labels (`LabelList`): values at bar ends, series names and last
* values at line ends, names inside areas. Default `true`.
*/
labels?: boolean;
/**
* The custom legend above the chart (beside it from 1024 px), with 16 px
* swatches showing the fill drawn. Default: shown for grouped or stacked
* bars with more than one series, where direct labels cannot name them.
*/
legend?: boolean;
/**
* Plot height in px on screen. Default: bars from their rows (16 px bars),
* other charts 320.
*/
height?: number;
/** Plot height in px in print. Default: the screen height. */
printHeight?: number;
/**
* Forces one ink: every slot in --primary12 and sequential cells as
* patterns. Not needed for the ground: the scope already draws one ink
* everywhere but a light base (`paper`, `white`) or a nested `white`
* plate (§8.7). Default `false`.
*/
oneInk?: boolean | null;
/**
* Primary Radix scale: even slots, slot 6, edges, axes and labels. Never
* defaulted; omitted, it inherits the scope [D133].
*/
primary?:
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null;
/**
* Secondary Radix scale: odd slots and sequential steps. Keep step 11 at
* 3:1 on the ground (§2 pairing matrix) [D128]. Never defaulted.
*/
secondary?:
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null;
/** The data-table trigger label. Default "Show Data Table". */
tableLabel?: React.ReactNode;
/** The trigger label while the table is open. Default "Hide Data Table". */
tableOpenLabel?: React.ReactNode;
}ChartSeries
One series: a numeric field of the rows, drawn in one pattern slot.
type ChartSeries<Key extends string = string> = {
/** The row field holding this series' values. */
key: Key;
/** The series name: direct label, legend entry, tooltip and table column head. */
name: string;
/**
* The pattern slot (§8.7): 1 solid, 2 hatch 45°, 3 dot screen, 4 hatch
* 0°, 5 crossed hatch, 6 outline ("Other", always the remainder).
* Default: the series' position, so the first series is the emphasis
* series. A series keeps its slot on screen, in print and in the legend.
*/
slot?: SeriesSlot;
}ChartSteps
A sequential encoding: each row’s step, 1–5, drawn as a stepped fill (§8.7).
type ChartSteps<Key extends string = string> = {
/** The row field holding the step, an integer from 1 to 5. */
key: Key;
/** The step's name, for the tooltip and the data-table column head. */
name: string;
/** End labels of the stepped legend, e.g. `['0/5', '5/5 available']`. */
labels?: [React.ReactNode, React.ReactNode];
}SequentialStep
type SequentialStep = 1 | 2 | 3 | 4 | 5SeriesSlot
type SeriesSlot = 1 | 2 | 3 | 4 | 5 | 6Specification: DESIGN-SYSTEM.md §8.7 (charts), §1.5.16 (data encodings) and §8.2 (the data table); decisions D125 and D167.