opsinjs
FoundationsExpressionData visualisation

Chart anatomy

Axes, ticks, bands and gaps. The rule against a truncated health y-axis, why the scale is fixed rather than fitted, and what a missing reading must look like.

Overview

The most consequential decision in a health chart is not the colour of the line. It is the range of the y-axis, and it is usually made automatically by whatever plotting library is in use, whose default is to fit the axis to the data.

Fitting the axis to the data is how a two-point change in a blood-pressure reading becomes a cliff. The library did nothing wrong; it filled the space it was given. But a reader does not read an axis, they read a shape, and the shape now says something the numbers do not.

So opsinjs states it as a rule with no soft edges: a health chart's y-axis is chosen once per metric and held. It does not fit itself to the visible window, it does not rescale when a reading changes, and it is not chosen to make anything look like anything.

NOT IMPLEMENTED. This component does not exist in any released version of opsinjs. There is no package to install, no module to import and no props interface to generate code against. Everything on this page is a specification of intended behaviour and may change without notice. Do not write code against it.

A chart that renders every part named here is not built yet

trend-sparkline ships. It draws a single series with an optional reference band, and it is deliberately minimal: no axis furniture, no second series, no interaction. It renders almost none of what this page names, and no chart in the registry renders all of it. Everything else on this page is specification, and the specification is what an implementation will be held to.

PlannedRoadmapWhat “planned” means

How it works

The axis is fixed, not fitted. The product sets the range for a metric once, with clinical input, and uses it for every rendering of that metric. Two consequences follow immediately, and both are the point: a change of a given size looks the same size every week, and two charts of the same metric can be compared.

Zero, or the clinical context, and nothing in between. Zero is meaningful for steps, for minutes of activity and for doses taken. Wherever it is meaningful, the axis starts at zero, always, and a bar or area chart may start nowhere else. Where zero is not meaningful, which covers most vital signs, the axis instead spans the clinically relevant range for that metric with headroom on both sides. What it may never do is span only the observed data. "Never truncate a health y-axis" means precisely this: the axis is not permitted to crop to the data in order to fill the plot area.

If the axis does not start at zero, say so where the reader is looking. An axis label alone does not carry it; readers take the shape first and the numbers second, if at all. The chart says the range it covers in words next to the shape.

Aspect ratio is part of the claim. The same data in a tall narrow frame looks volatile and in a short wide frame looks flat. The aspect ratio for a metric is fixed alongside its axis, for the same reason.

Gaps are gaps. A day with no reading is drawn as a discontinuity, never bridged by a straight line. Connecting across a gap draws values that were never measured, and it is the most common way a chart asserts data it does not have. That is data states, rule 4, in a chart.

Time on the x-axis is real time. Readings are placed by when they were taken, not evenly by index. Five readings taken over three weeks with a fortnight's gap in the middle are not a smooth series of five, and drawing them evenly spaced turns an irregular record into an apparent routine.

Minimal furniture. Gridlines are the lightest mark on the chart or absent; ticks are few and land on values a person would say out loud; the axis carries its unit. The last data point is labelled directly with its value, because that is the number the reader came for and they should not have to read it off an axis.

No dual y-axes, ever. Two scales on one plot make the crossing point of two lines look meaningful when it is an artefact of two arbitrary ranges. Use two charts.

Using it

Decide the axis with the metric, not with the chart. The range belongs to "how this product displays blood pressure", alongside its units and its precision, not to one screen. See Numbers, units and precision.

Do

A fortnight of systolic readings on the metric's fixed axis, with the reference band behind them, the days with no reading left as gaps, and the latest value labelled directly.

Don’t

The same fortnight auto-scaled to the observed minimum and maximum, with the two missing days interpolated. A four-point variation fills the frame, the reader sees a dramatic rise, and two of the points on the line were invented by the renderer.

Do not animate the axis. A y-axis that rescales while the reader watches turns a data update into a visual event and makes two renderings incomparable.

Do not let the window change the scale. Switching from seven days to ninety changes what is on the x-axis. It must not change the y-axis, or the two views tell different stories about the same readings.

Give the reader the window, in words. "Last 14 days" beside the chart, not only inferable from tick labels. Somebody skimming takes the shape and the caption and nothing else.

Do not extrapolate, project or smooth. A trend line, a moving average or a projection drawn on the same axes as the readings is indistinguishable from the readings. If a smoothed series is genuinely useful, it is visually subordinate, labelled, and never the only line.

Tokens

Axis, gridline and tick colours come from the neutral roles; the reference band uses the neutral band tint from the chart ramp; spacing and label sizes come from the space and type scales. Chart anatomy owns no tokens of its own, and that is deliberate, because a chart with its own private geometry tokens is a chart nobody else can match.

This table has not been generated. It is produced by scripts/build-tokens.mts. Run pnpm run generate and reload.

Nothing on this site types a measured number by hand, so an ungenerated table shows this rather than an example.

Accessibility impact

  • A fixed axis is an accessibility feature as much as an honesty one. Readers with low numeracy rely most heavily on the shape, which makes them the most exposed to a shape that changes meaning between renderings.
  • Axis labels scale with the reader's text size, so the plot area shrinks rather than the labels. A chart that assumes a fixed label width overlaps its own axis at 1.3×, and the first thing lost is the unit.
  • Gridlines and ticks are non-text content and are measured against their background under WCAG 2.2 SC 1.4.11. Making them "subtle" is the most common way a chart fails it.
  • A gap must be perceivable without colour, which means a real discontinuity in the line rather than a lighter segment. It must also be stated in the table twin, where a missing value is an empty cell with a reason, not a blank.
  • The window and the axis range belong in the chart's text summary, so that a reader using a screen reader gets the same framing a sighted reader takes from the shape. The contract is on Accessible charts.
  • Accessible charts has the table twin and the summary sentence that carry everything this page draws.
  • Trends and change says when a change is large enough to be worth drawing at all.
  • Data states has the partial-data rule that gaps are an instance of.

On this page