Skip to content

LineChart

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/line-chart.json
See it liveA balance across a quarter, one week not measured, and the same series forced to a zero floor.→

A line, a column or a sparkline is a question about the data, not the drawing. A column says each period is its own sum — hours worked in a week, and there is no Wednesday-afternoon figure between two weeks. A line says the value existed the whole time and was sampled: a balance, a price, a temperature. Drawing a sum as a line claims readings nobody took; drawing a level as columns throws away the thing being watched. Sparkline is this shape with the axes taken away, for beside a figure.

<LineChart
points={snapshots.map((s) => ({ at: Date.parse(s.date), value: s.balance }))}
label={`Balance, ${money(first)} to ${money(last)}`}
formatTick={money}
footer={<><span>1 Jun</span><span>30 Sep</span></>}
/>

A hole breaks the line. Three drawings are possible and two of them lie: interpolating across invents a reading nobody took, and closing the gap up moves every later point — a chart that says the value was 40 in March when it was 40 in April. Breaking the line leaves the gap visible, keeps every other point where it belongs, and invents nothing. A lone reading between two holes keeps its place too: it has no line, but it is a measurement.

The floor is not zero unless you say so, and that is a deliberate departure from BarChart. A bar’s length is the quantity, so its baseline has to be zero or the length lies. A line’s subject is change, and a balance between 4,900 and 5,100 on a zero-based axis is a flat rule — the very thing the reader opened the chart to see. The ticks are what keep this honest: they say where the bottom is. For a count rather than a level — requests, errors — pass bounds={{ min: 0 }}.

Ticks land on round numbers, not on the range cut into equal parts. 4,900 to 5,100 in four gives 4,950 and 5,050, which nobody reads as a landmark; the step is the nearest 1, 2, 5 or 10 above what the range needs. A range too narrow to hold a round number gets no ticks rather than invented ones.

Straight segments, never a curve. A spline through measured points overshoots between them, inventing highs and lows that were never recorded — the same lie as interpolating across a gap, drawn more prettily.

The plot owns its height in pixels, for the reason BarChart does: a percentage against a parent with no height of its own resolves to zero, and the chart disappears without failing. The line is drawn with a non-scaling stroke, so the box can be any shape without the stroke going thick one way and thin the other.

The axis under the plot is the caller’s. footer takes two or three labels — usually the first and last reading — because an axis is not a place for a list, and only the caller knows what its readings are called.

Nothing measured draws a bare plot, not a chart of zeroes and not an empty box: the axis stays, and the words beside it say why there is no line.

The arithmetic is importable without React, from line-scale: boundsOf(points, stated), runs(points, bounds) for the drawable stretches, pathOf(run), ticksFor(bounds, wanted) and yOf(value, bounds).

Props

Read from the types in ui/line-chart.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.

PropTypeDefault
sizesm | md'md'
gutterbooleantrue
toneaccent | series | good | bad | muted'accent'
pointsrequiredPoint[]The readings, in order. value: null is a measurement not taken.
boundsPartial<Bounds>Override any edge of the plot. min: 0 for a count, where zero is the truth rather than a flattening.
labelrequiredstringWhat the chart as a whole says, for a reader who cannot see it.
ticksnumber4How many value ticks to aim for. They land on round numbers, so the count is a wish rather than a promise. 0 draws none.
formatTick(value: number) => stringTurns a tick into its label. Without it the number is drawn as it is - which is right for a count and wrong for money or a duration.
footerReactNodeThe labels under the axis - usually the first and last reading. Two or three, not one per point: the axis is not a place for a list.

And every prop of React's HTMLAttributes<HTMLDivElement> except children, passed on.

Accessibility

The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 17 rules applied and passed, and none failed. The accessibility report has every item.

History

Each release that changed ui/line-chart.tsx - the file a product copies - newest first.

  1. v0.39.0
    • feat let the frame go on paper, and keep a panel or a chart on one sheet 354d583
  2. v0.33.0breaking
    • feat no-raw-button and no-arbitrary-scale ac7a11a
  3. v0.24.0
    • feat a level between the readings, with a hole drawn as a hole 24fee8f