FairGarden Design

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

Fig. 1 Parkside logged the most volunteer hours in both seasons. Source: FairGarden sign-in sheets, 2026.
ChartBars.tsx
'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

Visits peaked in June and eased through the late summer. Source: Gate counters, April to September 2026.
ChartColumns.tsx
'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

Warblers overtook sparrows in 2024 as the hedgerows matured. Source: Spring bird count, one morning each May.
ChartLines.tsx
'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)

Yard waste drives the autumn rise; kitchen scraps grow steadily.
ChartAreas.tsx
'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.

1/5 low5/5 high

Households waiting for a plot

The longest waiting lists sit where demand is rated highest.
ChartSteps.tsx
'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)

2026 against the two years before it: one series solid, the rest outlined.
  • 2024
  • 2025
  • 2026

Harvest (kg)

The same data in one ink, as on a deep or saturated ground: patterns carry the series.
ChartHighlight.tsx
'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.

PropTypeDescription
caption
React.ReactNode | undefined

The figure caption, which is the chart’s title: say what to notice.

categoryKey
(string & string) | number | symbol | undefined

The row field naming each category (bar label, column or time tick).

categoryLabel
string | undefined

Head of the category column in the data table. Default “Category”.

data
{}[] | undefined

The rows, one per category, in display order.

figureLabel
React.ReactNode | undefined

The figure label, e.g. “Fig. 3″, on its own line above the caption.

formatValue
((value: number) => string) | undefined

Formats every value: labels, ticks, tooltip and table. Default: grouped digits (toLocaleString('en-US')).

height
number | undefined

Plot height in px on screen. Default: bars from their rows (16 px bars), other charts 320.

highlight
(string & string) | number | symbol | undefined

Single highlight: this series takes slot 1 and every other series slot 6 (outline), the preferred form for “one against the rest”.

kind
'area' | 'line' | 'bar' | 'column' | null | undefined

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.

labels
boolean | undefined

Direct labels (LabelList): values at bar ends, series names and last values at line ends, names inside areas. Default true.

legend
boolean | undefined

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
boolean | null | undefined

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.

primary
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null
| undefined

Primary Radix scale: even slots, slot 6, edges, axes and labels. Never defaulted; omitted, it inherits the scope [D133].

printHeight
number | undefined

Plot height in px in print. Default: the screen height.

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
| undefined

Secondary Radix scale: odd slots and sequential steps. Keep step 11 at 3:1 on the ground (§2 pairing matrix) [D128]. Never defaulted.

series
| ChartSeries<(string & string) | number | symbol>[]
| undefined

The series, in the semantic order: at most 4 (6 at the hard limit; beyond that, small multiples). Extra series are not drawn.

source
React.ReactNode | undefined

Source and method notes, e.g. “Source: Trail counters, 2026.”

stacked
boolean | undefined

Stacks bar and column series into one bar per category. Area series always stack.

steps
| ChartSteps<(string & string) | number | symbol>
| undefined

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
React.ReactNode | undefined

The data-table trigger label. Default “Show Data Table”.

tableOpenLabel
React.ReactNode | undefined

The trigger label while the table is open. Default “Hide Data Table”.

valueLabel
string | undefined

Value axis title with its units, e.g. “Visitors (thousands)".

Additional types

chart
type chart = chart
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 | 5
SeriesSlot
type SeriesSlot = 1 | 2 | 3 | 4 | 5 | 6

Specification: DESIGN-SYSTEM.md §8.7 (charts), §1.5.16 (data encodings) and §8.2 (the data table); decisions D125 and D167.