# Chart

> Themed charts on Recharts: line, area, bar and donut in one frame with a table view, plus sparklines and a stat strip.

Category: Data display. Docs page: https://rime.mjsons.net/components/chart

## Installation

```bash
npx shadcn@latest add KezLahd/rime/charts
```

The chart layer lives in components/charts. Every chart mounts in ChartFrame: a caption, a legend, the plot and a Chart or Table toggle whose table is the same data for anyone who cannot read the picture. Loading, refreshing, empty and error states are built in (status).

Charts carry no hex. Every mark reads a custom property (--chart-1 to --chart-6, --chart-seq-*, --chart-good and friends) declared in tokens.css, and chart chrome (grid, axes, ticks) derives from the global tokens, so a preset, dark mode or a Rime Studio theme re-themes them with no client code. Tooltips glide between points (TOOLTIP_MOTION: Recharts' 400ms ease, off under reduced motion) and use the compact shadcn layout on the popover glass, with dot, line or dashed indicators.

The high-level charts take series (Rime's list) or config (a shadcn ChartConfig), and expose --color-<key> per series on the root. For anything custom, the shadcn-compatible primitives are here too: ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent and type ChartConfig. A shadcn chart example works with its Tailwind classes removed and ChartContainer given a height. The full gallery, with every tooltip variant, is at /charts.

Series take their colour in a fixed order (the colour-blind safety mechanism): never re-rank on filter, and past six series fold the rest into "Other" (foldSeries). summary is required: one or two sentences a screen reader hears in place of the plot.

StatStrip is the summary-card pattern for figures: one surface, hairline cells, each with a value, a delta in words and an optional Sparkline.

## Usage

```tsx
import { AreaChart, BarChart, ChartContainer, ChartTooltip, ChartTooltipContent, DonutChart, LineChart, Sparkline, StatStrip, type ChartConfig } from "@/components/charts";

<LineChart
  title="Orders per month"
  summary="Online orders rose from 182 in January to 344 in August."
  data={rows}
  x="month"
  series={[{ key: "online", label: "Online" }]}
/>
```

## Props: TrendChart

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title` (required) | `ReactNode` |  |  |
| `subtitle` | `ReactNode` |  |  |
| `summary` (required) | `string` |  | One or two sentences a screen reader hears in place of the picture: what is plotted and the headline ("Weekly orders rose from 8 to 32"). Also set as the SVG's <desc>. |
| `hideTitle` | `boolean` |  | Keeps the title for assistive tech but hides it, when a Group heading already says it. |
| `status` | `ChartStatus` |  |  |
| `emptyTitle` | `ReactNode` |  |  |
| `emptyDescription` | `ReactNode` |  | What to do next, or what will fill this. A bare "No data" is not an empty state. |
| `emptyAction` | `ReactNode` |  |  |
| `errorTitle` | `ReactNode` |  |  |
| `errorDescription` | `ReactNode` |  |  |
| `onRetry` | `() => void` |  |  |
| `actions` | `ReactNode` |  | Controls placed beside the table toggle (a segmented control, a menu). |
| `height` | `number` | `248` | Plot height in px, x-axis band included. |
| `className` | `string` |  |  |
| `data` (required) | `ReadonlyArray<ChartRow>` |  |  |
| `x` (required) | `string` |  | Field holding the x value (an ISO date for time series). |
| `formatX` | `(value: string) => string` | `String` | Axis tick: short ("23 Nov"). |
| `formatXLong` | `(value: string) => string` |  | Tooltip header and table rows: in full ("Week of 23 Nov 2026"). |
| `formatValue` | `(value: number) => string` | `formatNumber` |  |
| `xLabel` | `string` | `"Week"` | Table header for the x column. |
| `endLabels` | `boolean` | `true` | Value and name at each line's end. On by default for up to four series; switched off automatically where the ends would collide. |
| `reference` | `{ value: number; label: string }` |  | One horizontal limit on the same axis (program places). Never a second axis. |
| `tooltipTag` | `string` |  | Tooltip header tag, e.g. "Cumulative". |
| `series` (required) | `ReadonlyArray<ChartSeries> \| never` |  |  |
| `config` | `never \| ChartConfig` |  |  |

## Props: AreaChart

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title` (required) | `ReactNode` |  |  |
| `subtitle` | `ReactNode` |  |  |
| `summary` (required) | `string` |  | One or two sentences a screen reader hears in place of the picture: what is plotted and the headline ("Weekly orders rose from 8 to 32"). Also set as the SVG's <desc>. |
| `hideTitle` | `boolean` |  | Keeps the title for assistive tech but hides it, when a Group heading already says it. |
| `status` | `ChartStatus` |  |  |
| `emptyTitle` | `ReactNode` |  |  |
| `emptyDescription` | `ReactNode` |  | What to do next, or what will fill this. A bare "No data" is not an empty state. |
| `emptyAction` | `ReactNode` |  |  |
| `errorTitle` | `ReactNode` |  |  |
| `errorDescription` | `ReactNode` |  |  |
| `onRetry` | `() => void` |  |  |
| `actions` | `ReactNode` |  | Controls placed beside the table toggle (a segmented control, a menu). |
| `height` | `number` |  | Plot height in px, x-axis band included. |
| `className` | `string` |  |  |
| `data` (required) | `ReadonlyArray<ChartRow>` |  |  |
| `x` (required) | `string` |  | Field holding the x value (an ISO date for time series). |
| `formatX` | `(value: string) => string` |  | Axis tick: short ("23 Nov"). |
| `formatXLong` | `(value: string) => string` |  | Tooltip header and table rows: in full ("Week of 23 Nov 2026"). |
| `formatValue` | `(value: number) => string` |  |  |
| `xLabel` | `string` |  | Table header for the x column. |
| `endLabels` | `boolean` |  | Value and name at each line's end. On by default for up to four series; switched off automatically where the ends would collide. |
| `reference` | `{ value: number; label: string }` |  | One horizontal limit on the same axis (program places). Never a second axis. |
| `tooltipTag` | `string` |  | Tooltip header tag, e.g. "Cumulative". |
| `series` (required) | `ReadonlyArray<ChartSeries> \| never` |  |  |
| `config` | `never \| ChartConfig` |  |  |
| `stacked` | `boolean` |  | Parts of a whole over time. Off for a single running total. |

## Props: BarChart

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title` (required) | `ReactNode` |  |  |
| `subtitle` | `ReactNode` |  |  |
| `summary` (required) | `string` |  | One or two sentences a screen reader hears in place of the picture: what is plotted and the headline ("Weekly orders rose from 8 to 32"). Also set as the SVG's <desc>. |
| `hideTitle` | `boolean` |  | Keeps the title for assistive tech but hides it, when a Group heading already says it. |
| `status` | `ChartStatus` |  |  |
| `emptyTitle` | `ReactNode` |  |  |
| `emptyDescription` | `ReactNode` |  | What to do next, or what will fill this. A bare "No data" is not an empty state. |
| `emptyAction` | `ReactNode` |  |  |
| `errorTitle` | `ReactNode` |  |  |
| `errorDescription` | `ReactNode` |  |  |
| `onRetry` | `() => void` |  |  |
| `actions` | `ReactNode` |  | Controls placed beside the table toggle (a segmented control, a menu). |
| `height` | `number` |  | Plot height in px, x-axis band included. |
| `className` | `string` |  |  |
| `data` (required) | `ReadonlyArray<ChartRow>` |  |  |
| `category` (required) | `string` |  | Field holding the category (region, team member, week, stage). |
| `orientation` | `"columns" \| "bars" \| "columns" \| "bars"` | `"columns"` | columns = vertical bars, for time and short labels. bars = horizontal, for many or long-named categories (regions, team members). |
| `stacked` | `boolean` | `false` | Parts of a whole per category. |
| `categoryColors` | `ReadonlyArray<string>` |  | One colour per category, for ordered categories on a single series (a funnel's stages, from ordinalColors). Nominal categories never get this: one series is one colour. |
| `formatCategory` | `(value: string) => string` | `String` |  |
| `formatCategoryLong` | `(value: string) => string` |  |  |
| `formatValue` | `(value: number) => string` | `formatNumber` |  |
| `categoryLabel` | `string` | `"Category"` |  |
| `valueLabels` | `boolean` |  | Value at each bar's end. Default: on for a single unstacked series. |
| `max` | `number` |  | Fixed value-axis top, e.g. 5 for places per team member. |
| `categoryWidth` | `number` | `150` | Width reserved for horizontal-bar category labels. |
| `tooltipTag` | `string` |  |  |
| `series` (required) | `ReadonlyArray<ChartSeries> \| never` |  |  |
| `config` | `never \| ChartConfig` |  |  |

## Props: DonutChart

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `summary` (required) | `string` |  | One or two sentences a screen reader hears in place of the picture: what is plotted and the headline ("Weekly orders rose from 8 to 32"). Also set as the SVG's <desc>. |
| `title` (required) | `ReactNode` |  |  |
| `className` | `string` |  |  |
| `subtitle` | `ReactNode` |  |  |
| `hideTitle` | `boolean` |  | Keeps the title for assistive tech but hides it, when a Group heading already says it. |
| `status` | `ChartStatus` |  |  |
| `emptyTitle` | `ReactNode` |  |  |
| `emptyDescription` | `ReactNode` |  | What to do next, or what will fill this. A bare "No data" is not an empty state. |
| `emptyAction` | `ReactNode` |  |  |
| `errorTitle` | `ReactNode` |  |  |
| `errorDescription` | `ReactNode` |  |  |
| `onRetry` | `() => void` |  |  |
| `actions` | `ReactNode` |  | Controls placed beside the table toggle (a segmented control, a menu). |
| `data` (required) | `ReadonlyArray<DonutDatum>` |  |  |
| `centreLabel` (required) | `string` |  | Centre figure label, e.g. "Customers". The figure is the total. |
| `centreValue` | `string` |  | Override the centre figure (defaults to the sum). |
| `formatValue` | `(value: number) => string` | `formatNumber` |  |
| `size` | `number` | `168` | Ring diameter in px. |
| `categoryLabel` | `string` | `"Group"` |  |

## Props: StatStrip

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `stats` (required) | `ReadonlyArray<Stat>` |  |  |
| `loading` | `boolean` |  |  |
| `className` | `string` |  |  |
| `aria-label` | `string` |  |  |

## Props: Sparkline

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `values` (required) | `ReadonlyArray<number>` |  |  |
| `width` | `number` | `96` |  |
| `height` | `number` | `28` |  |
| `emphasiseLast` | `boolean` | `true` | The history in the de-emphasis hue, the current period (last segment and end dot) in the accent. Set false for an all-accent line. |
| `color` | `string` | `"var(--chart-1)"` | Accent colour, a var() from palette.ts. |
| `zeroBased` | `boolean` | `true` | Include zero in the scale. On for counts, off for rates that never near it. |
| `className` | `string` |  |  |

## Props: ChartContainer

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `config` (required) | `ChartConfig` |  |  |
| `children` (required) | `ComponentProps<typeof ResponsiveContainer>["children"]` |  | One Recharts chart (AreaChart, BarChart...). |
| `initialDimension` | `{ width: number; height: number }` | `{ width: 320, height: 200 }` | Size used before the first measure, so SSR and first paint are not 0 by 0. |

Also accepts the native attributes of: AriaAttributes, DOMAttributes, HTMLAttributes.

## Props: ChartTooltipContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` |  |  |
| `indicator` | `Indicator` | `"dot"` | dot (default), line or dashed key beside each row. |
| `hideLabel` | `boolean` | `false` |  |
| `hideIndicator` | `boolean` | `false` |  |
| `labelKey` | `string` |  | Config key to read the label from, when the label is not the x value. |
| `nameKey` | `string` |  | Row name from this field of the datum (pies: "browser"). |
| `color` | `string` |  | Forces every indicator to one colour. |
| `labelFormatter` | `(label: ReactNode, payload: ReadonlyArray<TooltipPayloadEntry>) => ReactNode` |  | Header text. Receives the resolved label and the payload. |
| `formatter` | `( value: TooltipPayloadEntry["value"], name: TooltipPayloadEntry["name"], item: TooltipPayloadEntry, index: number, datum: unknown, ) => ReactNode` |  | Replaces a row's whole body. |
| `valueFormatter` | `(value: number) => string` | `formatNumber` | Formats each numeric value (kit default: formatNumber). |
| `tag` | `string` |  | Small uppercase tag beside the label ("Cumulative"). Kit extra. |
| `total` | `boolean \| string` |  | Adds a total row: stacked charts. Kit extra. |

Also accepts the native attributes of: DOM attributes, Props.

## Props: ChartLegendContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` |  |  |
| `hideIcon` | `boolean` | `false` |  |
| `nameKey` | `string` |  |  |
| `payload` | `ReadonlyArray<LegendPayload>` |  |  |
| `verticalAlign` | `"top" \| "middle" \| "bottom"` | `"bottom"` |  |

## Examples

### Area, two series

```tsx
<AreaChart
  title="Orders per month"
  summary="Online orders rose every month, from 182 to 344."
  data={ORDERS}
  x="month"
  series={[{ key: "online", label: "Online" }, { key: "retail", label: "Retail" }]}
  formatX={formatMonthTick}
  formatXLong={formatMonth}
/>
```

### Line with a reference

```tsx
<LineChart title="Online orders" summary="…" data={ORDERS} x="month" series={[{ key: "online", label: "Online" }]} reference={{ value: 300, label: "Target" }} />
```

### Bars, labelled

```tsx
<BarChart title="Revenue by region" summary="North leads with 48,200." data={REVENUE} category="region" series={[{ key: "revenue", label: "Revenue" }]} orientation="bars" valueLabels />
```

### Donut

```tsx
<DonutChart title="Projects by status" summary="Of 40 projects, 22 are on track." centreLabel="Projects" data={STATUS_SPLIT} />
```

### Stat strip with sparklines

```tsx
<StatStrip aria-label="This month" stats={[{ label: "Orders", value: 505, delta: { value: 32, period: "vs last month" }, trend: [302, 323, 362, 375, 416, 453, 471, 505] }]} />
```

### The shadcn primitives

ChartContainer with a ChartConfig, Recharts marks reading var(--color-<key>), and ChartTooltipContent. See /charts for every tooltip variant.

```tsx
const chartConfig = {
  online: { label: "Online", color: "var(--chart-1)" },
  retail: { label: "Retail", color: "var(--chart-3)" },
} satisfies ChartConfig;

<ChartContainer config={chartConfig} style={{ aspectRatio: "auto", height: 220 }}>
  <BarChart accessibilityLayer data={rows}>
    <CartesianGrid vertical={false} />
    <XAxis dataKey="month" tickLine={false} axisLine={false} />
    <ChartTooltip {...TOOLTIP_MOTION} content={<ChartTooltipContent indicator="dot" />} />
    <Bar dataKey="online" fill="var(--color-online)" radius={4} />
    <Bar dataKey="retail" fill="var(--color-retail)" radius={4} />
  </BarChart>
</ChartContainer>
```

## Theme tokens

Per-component tokens. Undeclared by default: the default below is the fallback the CSS uses. Set any of them at :root or on a wrapper to restyle every instance.

| Token | Default |
| --- | --- |
| `--chart-axis` | `var(--line)` |
| `--chart-band` | `var(--wash-hover)` |
| `--chart-cursor` | `var(--line)` |
| `--chart-grid` | `var(--line-soft)` |
| `--chart-tick` | `var(--ink-muted)` |
| `--popover-blur` | `none` |
| `--popover-border` | `transparent` |
| `--popover-shadow` | `var(--shadow-float)` |
| `--popover-surface` | `var(--surface-solid)` |
| `--r-circle` | `50%` |
| `--r-flat` | `2px` |
| `--stat-count` | `4` |

System tokens it reads: `--brand`, `--danger-fill`, `--danger-ink`, `--dur`, `--dur-slow`, `--ease`, `--focus-ring`, `--font-body`, `--glass-fill-nested`, `--indicator`, `--ink-body`, `--ink-brand`, `--ink-disabled`, `--ink-heading`, `--ink-muted`, `--ink-secondary`, `--line`, `--line-glass`, `--line-soft`, `--r-2xs`, `--r-md`, `--r-sm`, `--r-xs`, `--rgb-brand`, `--rgb-brand-deep`, `--shadow-float`, `--success-ink`, `--surface-solid`, `--surface-sunken`, `--table-cell-pad`, `--table-head-fill`, `--table-head-ink`, `--table-head-pad`, `--table-head-rule`, `--table-row-hover`, `--table-row-line`, `--text-caption`, `--text-micro`, `--text-small`, `--tracking-label`, `--wash-active`, `--wash-hover`, `--wash-subtle`.

## Accessibility

- Every chart is a <figure> labelled by its title and described by summary, which is also the SVG's <desc>.
- The Chart or Table toggle shows the same data as a real table with a caption, for screen readers and for anyone comparing values.
- Colour is never the only key: legends name each series, end labels sit on lines, status hues always come with a label.
- The categorical order is the colour-blind safety mechanism; keep it.

## Do

- Write summary as the headline: what changed and by how much.
- Use StatStrip for headline figures: one surface, hairline cells, never a grid of separate cards.
- Pass status="loading" while data loads so the skeleton matches the chart's shape.

## Don't

- Don't hard-code a hex on a mark; set --chart-* tokens instead.
- Don't use more than six series; fold the rest into Other.
- Don't use status colours (good, warning, danger) as series identity.

Related: https://rime.mjsons.net/components/meter, https://rime.mjsons.net/components/table
