Line Chart
Token-driven line chart built on Recharts v3 — multi-series, step and curve interpolation, honest data gaps, y-axis domain control, threshold annotations and full light/dark theming.
Playground
Installation
pnpm add @tessinaui/ui rechartsUsage
import { LineChart } from "@tessinaui/ui";
import type { ChartConfig } from "@tessinaui/ui";
const config: ChartConfig = {
revenue: { label: "Revenue", color: "var(--chart-1)" },
expenses: { label: "Expenses", color: "var(--chart-3)" },
};
const data = [
{ month: "Jan", revenue: 4200, expenses: 2400 },
{ month: "Feb", revenue: 3800, expenses: 2210 },
{ month: "Mar", revenue: 5100, expenses: 2290 },
];
<LineChart
data={data}
config={config}
categoryKey="month"
dashedSeries={["expenses"]}
label="Revenue outgrew expenses every month this quarter"
/>Examples
Default
A smooth, single-series line.
Multiple
Two series in brand tones; expenses dashed so it stays distinct without colour.
Curve
All six interpolations, including the three step alignments.
Sizes
Three aspect ratios — sm (short/wide), md, and lg (tall).
Dots and labels
Marker modes, and the three ways to print values on the plot.
Missing data
What a null does, and what connectNulls does to it.
Y-axis domain
Anchored at zero, fitted to the data, or pinned to a fixed window.
Axes
Titles, tick formatters, a right-hand axis, and end-only ticks.
Thresholds and bands
ReferenceLine and ReferenceArea passed as children.
Forecast
Actual and projected as two series, the projection dashed.
Period comparison
The previous period as a muted, dashed ghost.
Series toggling
Toggles outside the legend, as real aria-pressed buttons.
Sparkline
The same component with its chrome switched off.
In a card
Range chips and the value callout belong to the card, not the chart.
States
Loading skeleton, empty state and error.
Missing data
A null leaves a gap, and that is the default. A gap is information: it says
nobody measured this point.
connectNulls bridges the gap by drawing a straight segment between the two
readings that surround it. That segment is an interpolation — a value nobody
recorded, drawn in the same stroke as the data. Turn it on only when a gap
genuinely means "unchanged since the last reading".
// Honest: Wednesday is missing, and the chart says so.
<LineChart data={readings} config={config} categoryKey="day" />
// Only when a gap really means "no change".
<LineChart data={readings} config={config} categoryKey="day" connectNulls />Choosing a y-axis
The axis is anchored at zero by default. autoMinValue fits it to the data
instead.
A truncated y-axis is legitimate on a line chart. A line encodes change, and a 4% movement on a zero-anchored axis is a flat line that tells the reader nothing. This is exactly the opposite of a bar chart, where length encodes magnitude and cropping the axis misstates the data.
Use minValue / maxValue when a chart must stay comparable across reloads, or
when the scale is externally defined (a 0–100 score, a target band).
Non-colour differentiation
Colour alone is unreliable — for colour-vision-deficient readers, in greyscale
print, and on a bad projector. LineChart gives you three encodings to layer on
top of hue:
- Markers —
dots="all"gives each line a dotted trail;dots="last"marks only the current reading. - Dashes —
dashedSeries={["expenses"]}renders named series with a dash pattern. This is also the convention for a projection or a previous period. - Direct labels —
showLabels="endpoints"or"extremes"prints values on the plot, so a series can be read without consulting the legend at all.
Every series stroke is 2px, which is the minimum width for a chart mark to stay
legible, and the --chart-* tokens are gated at 3:1 against the plot background.
Accessibility
- Name the insight, not the anatomy.
labelbecomes the chart's accessible name and renders it as arole="figure". "Visitors doubled since March" is useful; "Line chart of visitors by month" repeats what the page already says. descriptionadds a visually hidden summary, so a static read carries the message without anyone having to interact with the plot.accessibilityLayeris on: the plot is keyboard-navigable point by point, and the tooltip is a polite live region.- Animation respects
prefers-reduced-motion: reduce.
Composition
children are rendered inside the chart, so anything Recharts draws is
available without a prop for each one — ReferenceLine, ReferenceArea,
ReferenceDot, Brush, LabelList.
It is also the escape hatch for a fully custom tooltip: switch the built-in one off and pass your own.
<LineChart data={data} config={config} categoryKey="month" showTooltip={false}>
<ChartTooltip content={<MyTooltip />} />
<ReferenceLine y={target} label="Target" />
</LineChart>Boundaries
- Range presets (1D/1W/1M/1Y), the big value callout, and a granularity switcher belong to the surrounding card, not to the chart. No surveyed design system ships them as chart props — see the "In a card" example.
- A filled area under the line is
AreaChart, not a prop here. - A single value against a target is
MeterorProgress. - Categorical comparison is
BarChart; a line implies a continuous axis.
Theming
| Element | Token |
|---|---|
| Series stroke / marker | --chart-1 … --chart-6 (via config[key].color) |
| Grid + axis lines | border |
| Axis tick labels and titles | muted-foreground |
| Printed value labels | foreground |
| Tooltip surface | popover / popover-foreground / border |
Core props
| Prop | Type | Default | Description |
|---|---|---|---|
data | Record<string, string | number | null>[] | — | Row-per-category data. |
config | ChartConfig | — | Series metadata (label, color). |
categoryKey | string | — | Datum key for the x-axis. |
series | string[] | all config keys | Which series to plot. |
curve | "monotone" | "natural" | "linear" | "step" | "stepBefore" | "stepAfter" | "monotone" | Interpolation. |
size | "sm" | "md" | "lg" | "md" | Aspect ratio. |
dots | "none" | "all" | "last" | "all" | Point markers. "last" marks each series' final reading. |
showLabels | boolean | "endpoints" | "extremes" | false | Print values on the plot. |
dashedSeries | string[] | [] | Series rendered with a dashed stroke. |
connectNulls | boolean | false | Bridge null gaps with an interpolated segment. |
autoMinValue | boolean | false | Fit the y-axis to the data instead of anchoring at zero. |
minValue / maxValue | number | — | Pin either end of the y-axis. |
yAxisPosition | "left" | "right" | follows dir | Which side the y-axis sits on. |
xAxisLabel / yAxisLabel | string | — | Axis titles. |
xTickFormatter / yTickFormatter | (value, index) => string | — | Format tick text. |
startEndOnly | boolean | false | Render only the first and last x ticks. |
showGrid / showLegend / showXAxis / showYAxis / showTooltip | boolean | — | Toggle chart chrome. |
label | string | — | Accessible name; renders as role="figure". |
description | string | — | Visually hidden summary. |
children | ReactNode | — | Extra Recharts children (ReferenceLine, Brush, …). |
dir | "ltr" | "rtl" | "ltr" | Reading direction. |
label | string | — | Accessible name; renders the chart as a role="figure". |
description | string | — | Visually hidden summary for assistive tech. |
loading | boolean | false | Render the skeleton. |
error | ReactNode | — | Render an error message instead of the chart. |
emptyState | ReactNode | "No data to display" | Shown when data is empty. |
Migration
connectNulls now defaults to false. A chart whose data contains null
previously drew straight through the gap; it now breaks the line there. Pass
connectNulls to restore the old rendering — but read "Missing data" above
first, because the bridged segment is a value nobody measured.
showDots is deprecated in favour of dots. showDots keeps working for
one minor and warns once in development; dots wins if both are set.
| Before | After |
|---|---|
showDots | dots="all" (the default) |
showDots={false} | dots="none" |