Skip to content

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.

Revenue by month
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:

ts
const currency = { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }

Area ​

area adds a wash beneath the line. It is ignored for multiple series.

Revenue by month
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
Revenue by month and region
Category NorthSouthEast
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
Revenue by month and region, stacked
Category NorthSouthEast
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.

Daily balance, axis fitted to the data
Category Value
Mon€8,120
Tue€8,240
Wed€8,190
Thu€8,310
Fri€8,260
Daily balance, axis from zero
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.

Revenue by month, with two months missing
Category Value
Jan€18,200
Feb€22,400
Mar—
Apr€27,600
May€31,200
Jun—
Jul€36,400
Aug€34,100

Bare ​

Revenue by month
Category Value
Jan18,200
Feb22,400
Mar19,800
Apr27,600
May31,200
Jun29,900
Jul36,400
Aug34,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.

Revenue, straight
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
Revenue, curved
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.

Revenue, filling the width
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.

Revenue by month
Category Value
Jan18,200
Feb22,400
Mar19,800
Apr27,600
May31,200
Jun29,900
Jul36,400
Aug34,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 ​

PropTypeDefaultDescription
dataLineChartDatum[][]One entry per category, in order
seriesChartSeries[]—Two or more measures per category
variant'line' | 'area' | 'stacked''line'area washes under a single series; stacked sits several on each other
tensionnumber0Curve between readings, 0 straight to 1 rounded
flushboolean—Carry the line and fill flat to the plot edges
heightnumber240Rendered height in px; width fills the container
ticksnumber5Target tick count; the axis lands on round numbers
axisbooleantrueDraw the value axis and its gridlines
animateboolean or { duration, easing }trueReveal on the first paint; false turns it off
legendbooleantrueOnly honoured for a single series
zerobooleanfalseForce zero onto the axis
localestring—Locale for every number in the chart
formatIntl.NumberFormatOptions—Options for every number in the chart
labelstring—Accessible name for the figure
unstyledboolean—Drop built-in classes
classstring—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 ​

SlotPropsWhen to use it
underlaylayout geometryDraw behind the line — a target band, a shaded period
overlaylayout geometryDraw in front — an annotation, a threshold line

Data shapes ​

ts
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.