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.
Two series with a previous period and a target
Last 14 days against the previous 14
| Day | This period | Previous period |
|---|---|---|
| Mar 1 | 820 | 672 |
| Mar 2 | 932 | 764 |
| Mar 3 | 901 | 739 |
| Mar 4 | 934 | 766 |
| Mar 5 | 1,290 | 1,058 |
| Mar 6 | 1,330 | 1,091 |
| Mar 7 | 1,320 | 1,082 |
| Mar 8 | 1,180 | 968 |
| Mar 9 | 1,250 | 1,025 |
| Mar 10 | 1,400 | 1,148 |
| Mar 11 | 1,380 | 1,132 |
| Mar 12 | 1,460 | 1,197 |
| Mar 13 | 1,510 | 1,238 |
| Mar 14 | 1,490 | 1,222 |
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}
/>Usage examples
Area with gaps
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
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 />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
activeIndexinstead 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
Intlwith thelocaleprop.
API reference
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 withhiddenSeriesor uncontrolled withdefaultHiddenSeries. activeIndex+onActiveIndexChangeshare one crosshair between charts with the same x values.onPointSelectruns 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) orsummary.
- 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
coloron series that a filter can remove, so colours follow the entity. - Use
dashedfor a comparison period or forecast, so it differs by more than colour. - Pass
locale(andxFormatwithtimeZonefor 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: The frame LineChart renders; use it directly for custom charts.
- BarChart: Comparisons across categories or discrete periods.
- Sparkline: A tiny trend without axes, for tables and cards.
- StatCard: One headline number with its trend.
Dependencies
| Kind | Depends on |
|---|---|
| npm packages | react |
| Monarch components | ChartFrame |
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
hiddenSeriesandactiveIndex. onPointSelectfrom 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