Charts
ILineChart
A line over ordered categories, in plain SVG with no charting dependency. Hovering anywhere in the plot drops a crosshair on the nearest category and marks the point.
| Category | Value |
|---|---|
| Jan | €18,200 |
| Feb | €22,400 |
| Mar | €19,800 |
| Apr | €27,600 |
| May | €31,200 |
| Jun | €29,900 |
| Jul | €36,400 |
| Aug | €34,100 |
The width fills the container and height (240px by default) is the only dimension you set. locale and format feed one Intl.NumberFormat, used for the ticks and the tooltip alike.
The examples below pass currency as format:
const currency = { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }Area
area adds a wash beneath the line. It is ignored for multiple series.
| Category | Value |
|---|---|
| Jan | €18,200 |
| Feb | €22,400 |
| Mar | €19,800 |
| Apr | €27,600 |
| May | €31,200 |
| Jun | €29,900 |
| Jul | €36,400 |
| Aug | €34,100 |
Several series
- North
- South
- East
| Category | North | South | East |
|---|---|---|---|
| Jan | €8,200 | €5,400 | €3,100 |
| Feb | €9,600 | €6,100 | €3,800 |
| Mar | €9,100 | €7,400 | €4,200 |
| Apr | €11,800 | €8,200 | €5,900 |
| May | €12,600 | €9,100 | €6,400 |
| Jun | €14,200 | €9,800 | €7,100 |
A legend appears automatically from two series up; legend is honoured only for a single series. When series come and go, give each one a slot so its colour stays with the entity.
Stacked
stacked sits each series on top of the one before it, so the bands abut instead of overlapping and the top edge is the total. It is the multi-series answer to area, which stays single-series because overlapping washes muddy into a colour belonging to neither.
- North
- South
- East
| Category | North | South | East |
|---|---|---|---|
| Jan | €8,200 | €5,400 | €3,100 |
| Feb | €9,600 | €6,100 | €3,800 |
| Mar | €9,100 | €7,400 | €4,200 |
| Apr | €11,800 | €8,200 | €5,900 |
| May | €12,600 | €9,100 | €6,400 |
| Jun | €14,200 | €9,800 | €7,100 |
Stacking reads as parts of a whole, so it carries two rules the other variants do not. The axis always includes zero, because a band's height is its value and measuring that from anywhere else makes the parts stop adding up. And a gap counts as no contribution rather than breaking the band — dropping every series above it by its own height would read as a fall in their numbers. The gap is still a gap in the exposed table, where the cell is empty rather than zero.
The tooltip reports each series' own reading, not its running total. Read the totals off the top edge.
Reach for it when the parts belong to one quantity — revenue by region, storage by bucket. When they are independent measures that happen to share an axis, keep them as lines: stacking implies a sum, and summing two unrelated numbers gives a total that means nothing.
Where the axis starts
Zero is off by default, unlike IBarChart: the axis is fitted to the data so the shape of the line stays readable.
| Category | Value |
|---|---|
| Mon | €8,120 |
| Tue | €8,240 |
| Wed | €8,190 |
| Thu | €8,310 |
| Fri | €8,260 |
| Category | Value |
|---|---|
| Mon | €8,120 |
| Tue | €8,240 |
| Wed | €8,190 |
| Thu | €8,310 |
| Fri | €8,260 |
Turn zero on when the question is about magnitude rather than movement: the first chart above draws a 2% wobble across the full height of the plot.
Gaps
null is a missing reading, and the line breaks rather than bridging it.
| Category | Value |
|---|---|
| Jan | €18,200 |
| Feb | €22,400 |
| Mar | — |
| Apr | €27,600 |
| May | €31,200 |
| Jun | — |
| Jul | €36,400 |
| Aug | €34,100 |
Bare
| Category | Value |
|---|---|
| Jan | 18,200 |
| Feb | 22,400 |
| Mar | 19,800 |
| Apr | 27,600 |
| May | 31,200 |
| Jun | 29,900 |
| Jul | 36,400 |
| Aug | 34,100 |
For something smaller still — a trend inside a table cell or a stat tile, with no axis, tooltip or crosshair at all — use ISparkline.
Curves
tension bends the line between readings, from 0 for straight segments to 1 for fully rounded.
| Category | Value |
|---|---|
| Jan | €18,200 |
| Feb | €22,400 |
| Mar | €19,800 |
| Apr | €27,600 |
| May | €31,200 |
| Jun | €29,900 |
| Jul | €36,400 |
| Aug | €34,100 |
| Category | Value |
|---|---|
| Jan | €18,200 |
| Feb | €22,400 |
| Mar | €19,800 |
| Apr | €27,600 |
| May | €31,200 |
| Jun | €29,900 |
| Jul | €36,400 |
| Aug | €34,100 |
It stays 0 by default, and that is a claim about the data rather than a taste. A curve says the readings run continuously into one another — true of a temperature trace, false of six monthly totals, where nothing happened between the points at all.
The control points are clamped to the pair of readings they sit between. An unclamped spline overshoots: between a low reading and a high one it swings past both, dipping under the smallest number in the data and over the largest. A chart that draws values nobody measured is worse than a chart with corners.
Filling the width
Readings sit a quarter of a category clear of each edge, so the first and last markers have somewhere to sit. flush carries the line and its fill flat out to the edges anyway, without moving the readings, the markers, the labels or the tooltip.
| Category | Value |
|---|---|
| Jan | €18,200 |
| Feb | €22,400 |
| Mar | €19,800 |
| Apr | €27,600 |
| May | €31,200 |
| Jun | €29,900 |
| Jul | €36,400 |
| Aug | €34,100 |
The extension is flat, so it never implies a value the series does not have — it holds the first and last readings level out to the edge. A series broken by a gap only extends the runs that actually reach an end of the data.
Animation
The plot is uncovered left to right: the line and its wash share one clip, so they arrive together.
| Category | Value |
|---|---|
| Jan | 18,200 |
| Feb | 22,400 |
| Mar | 19,800 |
| Apr | 27,600 |
| May | 31,200 |
| Jun | 29,900 |
| Jul | 36,400 |
| Aug | 34,100 |
animate takes false to switch the reveal off, or an object to tune it: duration in milliseconds and easing, one of ease-out (the default), ease-in, ease-in-out or linear.
Drawing the line on with a dash offset was tried and dropped. A dash advances along the path while a fill can only be uncovered along x, so the line and its wash drifted apart wherever the line was steep — one clip rectangle is what keeps them in step, whatever the curve.
It plays once, on the first paint with something to draw — not again when the data changes underneath it. A reader who has asked for reduced motion gets the finished chart with no animation at all.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
data | LineChartDatum[] | [] | One entry per category, in order |
series | ChartSeries[] | — | Two or more measures per category |
variant | 'line' | 'area' | 'stacked' | 'line' | area washes under a single series; stacked sits several on each other |
tension | number | 0 | Curve between readings, 0 straight to 1 rounded |
flush | boolean | — | Carry the line and fill flat to the plot edges |
height | number | 240 | Rendered height in px; width fills the container |
ticks | number | 5 | Target tick count; the axis lands on round numbers |
axis | boolean | true | Draw the value axis and its gridlines |
animate | boolean or { duration, easing } | true | Reveal on the first paint; false turns it off |
legend | boolean | true | Only honoured for a single series |
zero | boolean | false | Force zero onto the axis |
locale | string | — | Locale for every number in the chart |
format | Intl.NumberFormatOptions | — | Options for every number in the chart |
label | string | — | Accessible name for the figure |
unstyled | boolean | — | Drop built-in classes |
class | string | — | Merged with the built-in classes |
ui | { root?, svg?, grid?, tick?, category?, line?, area?, band?, crosshair?, marker?, markerRing?, tooltip?, tooltipLabel?, tooltipValue?, table? } | — | Per-element class overrides |
Slots
| Slot | Props | When to use it |
|---|---|---|
underlay | layout geometry | Draw behind the line — a target band, a shaded period |
overlay | layout geometry | Draw in front — an annotation, a threshold line |
Data shapes
interface LineChartDatum {
label: string
value?: number | null
[key: string]: unknown
}
interface ChartSeries {
key: string
name?: string
slot?: number
}The line is hidden from assistive technology and the data is exposed as a table instead, so a screen reader gets the numbers rather than a shape. label names that table.