Source: https://monarchui.dev/components/line-chart/

Components · Charts

# LineChart

Line and area charts for trends: multiple series, gaps, reference lines, crosshair tooltip on hover, touch and keyboard, legend toggles and a data table.

@rookcom/monarch-react/charts

Component

## Two series with a previous period and a target

## Usage examples

### Area with gaps

### Two charts sharing one crosshair

## Purpose

Shows how one or more measures change over time or across ordered categories, with exact values one hover, tap or arrow key away and the full data as a table.

### Use when

- A trend over days, weeks or months: traffic, sign-ups, revenue, latency.
- Comparing a few series on the same scale, or a period against the previous one (`dashed`).
- Showing a target, baseline or SLA against the trend (`referenceLines`).

### Avoid when

- Comparing unordered categories: use BarChart.
- A trend inside a table cell or stat card: use Sparkline.
- Two measures on different scales: draw two charts with a shared `activeIndex` instead of a second axis.

## Design principles

- One value axis. Different scales get separate charts, synced through `activeIndex`, never a dual axis.
- Gaps are honest: a missing value breaks the line instead of drawing a fake segment, and a lone point gets a dot.
- Recessive grid and axes, 2px lines, colour slots in fixed order; colours follow the series, so hiding one never repaints the rest.
- Exact values on demand (tooltip), not on every point.
- Every number and date goes through `Intl` with the `locale` prop.

## API reference

```tsx
import { LineChart } from '@rookcom/monarch-react/charts';
```

| Export | Kind | Rendering | Root class |
| --- | --- | --- | --- |
| `LineChart` | component | client | `.mn-chart` |

Renders ChartFrame (ref to its `<figure>`); every ChartFrame prop except `children`, `legend` and `table` passes through, and LineChart builds the legend and table from `series` and `data`.

`data` is an array of plain objects; `xKey` names the x field (Date, number or label) and each series `key` names a numeric field. Missing, `null` or non-finite values are gaps.

Pass `locale` when rendering on the server so server and browser format numbers and dates the same way.

Generated from the TypeScript declarations of `packages/react/src/components/charts/line-chart`.

## Properties

### `LineChart`

Line and area chart for trends over time or ordered categories, with multiple series, gaps, reference lines, a shared crosshair tooltip on hover, touch and keyboard, legend toggles and an accessible data table.

| Prop | Type | Description |
| --- | --- | --- |
| `data` required | `readonly ChartDatum[]` | Rows in x order. Each has the `xKey` field and one number per series; a missing or `null` value is a gap. |
| `series` required | `readonly ChartSeries[]` | The measures to draw. Two to five read best; colours follow the order unless pinned. |
| `title` required | `ReactNode` | The chart's name. Labels the figure. Required. |
| `xKey` required | `string` | Field holding the x value: a `Date`, a number or a category label. |
| `actions` | `ReactNode` | Period or metric controls in the header, such as a pill TabsFilter or a Select. |
| `activeIndex` | `number \| null` | Controlled inspected index (`null` for none), for a crosshair shared by charts on the same x values. Leave undefined for uncontrolled. |
| `className` | `string` |  |
| `curve` | `Curve` | `linear` (default) joins points with straight lines; `monotone` smooths without overshooting. |
| `defaultHiddenSeries` | `readonly string[]` | Uncontrolled initial hidden series. |
| `defaultShowTable` | `boolean` | Uncontrolled initial table view. Default `false`. |
| `description` | `ReactNode` | One line under the title: the period, unit or what the chart answers. Describes the figure. |
| `dots` | `boolean` | Draw a dot on every point. Default `false` (lone points between gaps always get one). |
| `emptyActions` | `ReactNode` | Actions in the empty state, such as a link to set up tracking. |
| `emptyDescription` | `ReactNode` | Empty-state description, such as what will appear and when. |
| `footer` | `ReactNode` | Notes under the chart: source, last updated, a footnote. |
| `formatTick` | `ValueFormatter` | Formats value-axis ticks. Default: compact notation of `valueFormat` (1.2K). |
| `formatValue` | `ValueFormatter` | Formats values in the tooltip, table and summary. Wins over `valueFormat`. |
| `formatX` | `((value: XValue, index: number) => string)` | Formats x values everywhere (axis, tooltip, table, summary). Wins over `xFormat`. |
| `headingLevel` | `ChartFrameHeadingLevel` | Render the title as a heading (`h2`–`h6`) to match the page outline. Default: not a heading. |
| `height` | `number` | Height reserved for the chart body (loading, empty and error keep it, so nothing jumps). Default 240. |
| `hiddenSeries` | `readonly string[]` | Controlled list of hidden series keys. |
| `id` | `string` |  |
| `includeZero` | `boolean` | Keep zero on the value axis. Default `true`; turn off for values far from zero. |
| `labels` | `LineChartLabels` |  |
| `legendToggle` | `boolean` | Legend entries toggle their series. Default `true` (legend shows for two or more series). |
| `locale` | `string` | BCP 47 locale for numbers and dates. Pass it when rendering on the server. |
| `onActiveIndexChange` | `((index: number \| null) => void)` |  |
| `onHiddenSeriesChange` | `((hidden: string[]) => void)` |  |
| `onPointSelect` | `((datum: ChartDatum, index: number) => void)` | Click, Enter or Space on the inspected point, for drill-in. |
| `onRetry` | `(() => unknown)` | Error-state retry. Return the refetch promise to keep the button busy until it settles. |
| `onShowTableChange` | `((showTable: boolean) => void)` |  |
| `referenceLines` | `readonly ChartReferenceLine[]` | Dashed horizontal lines for targets, baselines or SLAs. |
| `showTable` | `boolean` | Controlled table view. |
| `status` | `ChartFrameStatus` | `ready` (default) shows the chart; `loading` a skeleton with `aria-busy`; `empty` an empty state; `error` an error with Try again when `onRetry` is set. Exposed as `data-status`. |
| `summary` | `string` | Replaces the generated text summary (the plot's accessible name). |
| `tableToggle` | `boolean` | Show the "View as table" toggle. Default `true` when `table` is set. |
| `valueFormat` | `Intl.NumberFormatOptions` | `Intl.NumberFormat` options for values in the tooltip, table and summary. |
| `variant` | `LineChartVariant` | `line` (default) or `area`: the line plus a soft fill to the baseline. Exposed as `data-variant`. |
| `xFormat` | `Intl.DateTimeFormatOptions` | `Intl.DateTimeFormat` options for date x values. Default `{ month: 'short', day: 'numeric' }`. |
| `xLabel` | `string` | Column header for the x values in the data table. Default: `xKey`. |
| `xScale` | `ChartXScale` | `time` spaces points by their date or number (gaps in time show); `category` spaces them evenly. Default: `time` when every x is a Date or number. |
| `yDomain` | `readonly [ChartDomainBound, ChartDomainBound]` | Fix either end of the value axis, e.g. `[0, 100]` for percentages. |

## Variants

| Prop | Values | Description |
| --- | --- | --- |
| `variant` | `'line'``'area'` | `area` adds a soft fill from each line to the baseline. Exposed as `data-variant`. |
| `curve` | `'linear'``'monotone'` | `monotone` smooths the line without overshooting the data. |
| `xScale` | `'time'``'category'` | `time` (default when every x is a Date or number) spaces points by value, so uneven dates show; `category` spaces them evenly. |
| `series[].dashed` | `'true'``'false'` | A dashed line as a second encoding: previous period, forecast. |
| `dots` | `'true'``'false'` | A dot on every point; by default only lone points and the inspected point. |

## States

| State | Exposed as | Behaviour |
| --- | --- | --- |
| Inspecting | – | Crosshair, enlarged dots on each visible series and a tooltip with every visible value at that x. |
| Series hidden | `aria-pressed="false" on the legend toggle` | The series leaves the plot and the y range; it stays in the data table. |
| Empty | `data-status="empty"` | No rows: ChartFrame shows its empty state. |
| Loading / error | `data-status` | Pass `status` (and `onRetry`); the frame keeps the chart height. |
| Focus | – | The plot shows the Monarch focus ring on `:focus-visible`. |

## Interaction behaviour

- Hovering or tapping the plot inspects the nearest point; the tooltip flips to stay inside the chart.
- Moving the mouse out clears it; a tap keeps the last position until the next tap.
- Legend entries toggle their series (`legendToggle`), controlled with `hiddenSeries` or uncontrolled with `defaultHiddenSeries`.
- `activeIndex` + `onActiveIndexChange` share one crosshair between charts with the same x values.
- `onPointSelect` runs on click, Enter or Space at the inspected point, for drill-in.

## Keyboard support

| Keys | Action |
| --- | --- |
| Tab | Focuses the plot (one tab stop) and inspects the first point. |
| ArrowRight / ArrowDown | Next point. |
| ArrowLeft / ArrowUp | Previous point. |
| Home / End | First or last point. |
| PageUp / PageDown | Moves a tenth of the points. |
| Escape | Hides the tooltip. |
| Enter / Space | Calls `onPointSelect` for the inspected point. |

## Accessibility

Role

figure (ChartFrame); the plot is a group with aria-roledescription "chart"

Accessible name

The figure is named by

title

; the plot by a generated summary (points, then low / high / latest per series, then reference lines) or

summary

.

- The SVG is `aria-hidden`; the summary, a live region and the data table carry the information.
- While inspecting with the keyboard, a polite live region announces "Mar 4: Sessions 1,204, Previous 988".
- The tooltip is visual only (`aria-hidden`), so pointer hovering does not flood screen readers.
- A hint ("Use the arrow keys to inspect values.") is the plot's description.
- The data table lists every series, hidden ones included, with row and column headers.
- Series differ by label and, where set, dash pattern, not by colour alone.
- Every string, including the summary sentences, is translatable through `labels`.

## Responsive behaviour

- The chart measures its container (ResizeObserver) and lays out at that width, so text never scales; it works down to 320px.
- X labels thin out to keep about 72px between them; the first and last stay.
- Before the width is known (server render, first paint) it draws at 640px scaled to fit, so the markup matches on hydration.
- Touch: tap to inspect; vertical page scrolling stays available (`touch-action: pan-y`).

## Theme support

- Series use `--mn-color-data-1` … `-5`, contrast-checked per theme; a sixth series and later fall back to the neutral slot.
- Grid, ticks, crosshair and reference lines use border and text tokens; the tooltip uses the raised surface and shadow.
- Light, dark and high-contrast themes follow the tokens with no extra CSS. Reduced motion: the only motion is the tooltip fade, which uses duration tokens that become 0ms.

## Do and don't

### Do

- Pin `color` on series that a filter can remove, so colours follow the entity.
- Use `dashed` for a comparison period or forecast, so it differs by more than colour.
- Pass `locale` (and `xFormat` with `timeZone` for dates) for consistent server and client output.
- Fold a sixth series and beyond into "Other", or split the chart.

### Don't

- Put two measures with different units on one chart; use two charts with a shared `activeIndex`.
- Replace gaps with zeros: a missing day is not a zero day.
- Label every point; the tooltip and table carry exact values.

## Related components

- [ChartFrame](https://monarchui.dev/components/chart-frame/): The frame LineChart renders; use it directly for custom charts.
- [BarChart](https://monarchui.dev/components/bar-chart/): Comparisons across categories or discrete periods.
- [Sparkline](https://monarchui.dev/components/sparkline/): A tiny trend without axes, for tables and cards.
- [StatCard](https://monarchui.dev/components/stat-card/): One headline number with its trend.

## Dependencies

| Kind | Depends on |
| --- | --- |
| npm packages | `react` |
| Monarch components | [ChartFrame](https://monarchui.dev/components/chart-frame/) |

Hand-written SVG with no charting library (docs/adr/0010-charts-rendering-and-colour.md). Scales, paths, tooltip, legend and axes come from `src/internal/charts`.

## Testing coverage

- One line per series, legend, generated summary, keyboard hint; no axe violations.
- Keyboard: Tab focuses the plot and inspects the first point; arrows, Home, End, Escape; live region announces; leaving clears the tooltip.
- Hover inspects the nearest point with a crosshair and flipped tooltip, without announcing; leaving clears it.
- Gaps split the path and a lone point gets a dot; a single series has no legend.
- Legend toggle hides a series (uncontrolled) and keeps the other colour; controlled `hiddenSeries` and `activeIndex`.
- `onPointSelect` from Enter and click.
- Locale-aware numbers and dates (de-DE currency and long month).
- Area, monotone curve, dots and reference lines (also in the summary); fixed y domain and custom summary.
- Empty data shows the empty state; loading passes through; the table lists every series.
- Renders on the server at the fallback width.

| Check | Covered |
| --- | --- |
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery/line-chart; Cross-browser axe: /components/line-chart |

Tests: `packages/react/src/components/charts/line-chart`

## All example code

### Two series with a previous period and a target

```tsx
import { LineChart } from '@rookcom/monarch-react/charts';

const DAYS = Array.from({ length: 14 }, (_, i) => new Date(Date.UTC(2026, 2, 1 + i, 12)));

const SESSIONS = [820, 932, 901, 934, 1290, 1330, 1320, 1180, 1250, 1400, 1380, 1460, 1510, 1490];

const TRAFFIC = DAYS.map((date, i) => ({
  date,
  sessions: SESSIONS[i] ?? 0,
  previous: Math.round((SESSIONS[i] ?? 0) * 0.82),
}));

const UTC_DAY: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', timeZone: 'UTC' };

<LineChart
  title="Sessions"
  description="Last 14 days against the previous 14"
  data={TRAFFIC}
  xKey="date"
  xLabel="Day"
  series={[
    { key: 'sessions', label: 'This period' },
    { key: 'previous', label: 'Previous period', color: 'neutral', dashed: true },
  ]}
  referenceLines={[{ value: 1200, label: 'Target' }]}
  locale="en-US"
  xFormat={UTC_DAY}
/>
```

### Area with gaps

```tsx
import { LineChart } from '@rookcom/monarch-react/charts';

const DAYS = Array.from({ length: 14 }, (_, i) => new Date(Date.UTC(2026, 2, 1 + i, 12)));

const LATENCY = DAYS.map((date, i) => ({
  date,
  p95: [210, 220, null, null, 260, 240, 410, 380, 250, 230, 225, null, 240, 235][i] ?? null,
}));

const UTC_DAY: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', timeZone: 'UTC' };

<LineChart
  title="p95 latency"
  description="Days with no traffic are gaps"
  variant="area"
  curve="monotone"
  data={LATENCY}
  xKey="date"
  xLabel="Day"
  series={[{ key: 'p95', label: 'p95', color: 3 }]}
  includeZero={false}
  formatValue={(v) => `${v} ms`}
  locale="en-US"
  xFormat={UTC_DAY}
  height={180}
/>
```

### Two charts sharing one crosshair

```tsx
import { useState } from 'react';
import { LineChart } from '@rookcom/monarch-react/charts';

const DAYS = Array.from({ length: 14 }, (_, i) => new Date(Date.UTC(2026, 2, 1 + i, 12)));

const SESSIONS = [820, 932, 901, 934, 1290, 1330, 1320, 1180, 1250, 1400, 1380, 1460, 1510, 1490];

const SIGNUPS = [42, 51, 47, 60, 88, 91, 85, 70, 76, 95, 90, 99, 104, 101];

const TRAFFIC = DAYS.map((date, i) => ({
  date,
  sessions: SESSIONS[i] ?? 0,
  previous: Math.round((SESSIONS[i] ?? 0) * 0.82),
}));

const SIGNUP_DATA = DAYS.map((date, i) => ({ date, signups: SIGNUPS[i] ?? 0 }));

const UTC_DAY: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', timeZone: 'UTC' };

function SyncedDemo() {
  const [active, setActive] = useState<number | null>(null);
  return (
    <>
      <LineChart
        title="Sessions"
        data={TRAFFIC}
        xKey="date"
        xLabel="Day"
        series={[{ key: 'sessions', label: 'Sessions' }]}
        locale="en-US"
        xFormat={UTC_DAY}
        height={160}
        activeIndex={active}
        onActiveIndexChange={setActive}
      />
      <LineChart
        title="Sign-ups"
        data={SIGNUP_DATA}
        xKey="date"
        xLabel="Day"
        series={[{ key: 'signups', label: 'Sign-ups', color: 2 }]}
        locale="en-US"
        xFormat={UTC_DAY}
        height={160}
        activeIndex={active}
        onActiveIndexChange={setActive}
      />
    </>
  );
}

<SyncedDemo />
```
