Data
Sparkline
A run of numbers at the size of a word.
Usage
When to reach for it
Ships from@misoto22/design/charts
import { Sparkline } from '@misoto22/design/charts'Notes
A run of numbers at the size of a word — in a table cell, beside a figure, at the end of a row.
Deliberately axis-less, gridless and label-less: a sparkline answers "what shape has this been", and every piece of chrome that would make it answer "what value exactly" also makes it too big to sit inline, which was the only reason to reach for it. When the exact value matters, print the number beside it — value does — and when the trend needs reading precisely, it wants a <LineChart> and its own space.
No rendering engine: it is one <path> over a normalised viewBox, so it costs nothing to put a hundred of them in a table.
Anatomy
| Element | Description |
|---|---|
| Inline rowrequired | An inline-flex span, full width with 8px of gap, so the run sits in a table cell or beside a figure without breaking the line it is on. |
| Plotrequired | One svg role="img" over a 0–100 by 0–100 viewBox with preserveAspectRatio="none", so it stretches to whatever width the container gives it. height, 28px by default, is the only fixed dimension. |
| Markrequired | The path, its area fill, or the bars — variant picks one. The stroke is drawn with non-scaling-stroke, which is what keeps the line the same weight in a narrow cell and a wide one after the box has been stretched to fit. |
| Last point | A 2px dot on the final reading, from showLast, on the line and area variants. The bars variant carries the end of the run in its own last bar and draws no dot. |
| Printed value | value, in mono tabular figures after the plot. It is also what the accessible name says after the label, and it is the only figure this component ever prints. |
| Too-short state | What renders in place of the whole plot when fewer than two finite numbers survive: the label and “not enough data”, as one line of mono meta text. |
Best practices
Do
- Pass value whenever a figure matters. The plot has no axis and no scale, so it carries shape and nothing else, and value is both the one number printed and the reading appended to the accessible name. Left out, that name falls back to the last point through toLocaleString — the raw number, without the unit, the currency or the rounding the row beside it uses.
- Pin domain across any two that will be read against each other. Each run is normalised into the same fixed box from its OWN min and max, so the highest point always touches the top edge and the lowest always the floor: a series moving between 4 and 6 and a series moving between 400 and 900 draw the same silhouette, and the difference between them is drawn nowhere.
- Downsample a long run before handing it over. The x step is 100 divided by one less than the number of points, spread across whatever width the cell has, so four hundred readings in a 200px cell land half a pixel apart and the path fills in as a band.
- Read a flat line through the middle as “unchanged”, not as “at its floor”. A run whose min equals its max has a scale with no width, so no position on it is truer than another and every point sits at the centre — the same answer Heatmap and BulletChart give a zero span, and the one that keeps “unchanged” and “pinned at its worst” apart in a column of them.
Don’t
- Do not assume something chart-shaped always renders. Non-finite entries are filtered out first, and anything left under two points returns a line of text instead of an SVG, so the new account’s row is a sentence where every other row in the column is a chart.
Examples
variants
The three marks the same run can be drawn with: line reads a trend, area weights that trend toward volume, and bars separate the readings, which is what a discrete count wants. All three are axis-less, gridless and label-less by design — every piece of chrome that would let one answer "what value exactly" also makes it too big to sit inline, which was the only reason to reach for it. The exact figure is printed beside the run by value instead, and on line and area the last point is dotted, since that is usually the one being asked about.
in a table
A column of sparklines, every one of them pinned to the same domain. That is the whole lesson: on domains derived per row, each run peaks and troughs at the same heights, so a channel that halved and a channel that doubled draw the same picture — the single way a table of sparklines becomes actively misleading. Each run also carries its own label, because a screen reader meets four of these in four cells and nothing else in the row names them.
| Channel | Trend | This week |
|---|---|---|
| Organic search | 71k | |
| Paid social | 28k | |
| Direct | 29k | |
| Referral | 24k |
not enough data
What a sparkline does when there is no shape to draw. Under two points it prints its own label followed by "not enough data" instead of a mark: one reading is not a trend, and normalising a single value against itself puts a dot wherever the arithmetic lands, which a reader would take for a measurement. It is the state a new project is in on its first day and the state a narrow filter reaches constantly, so it is worth knowing what it looks like before it turns up forty rows down a table.
Types
export type SparklineVariant = 'line' | 'area' | 'bars'Accessibility
- label is required and is the whole accessible name: a sparkline has no axes and no legend, so nothing else describes it.
- Axis-less by design. Every piece of chrome that would let it answer “what value exactly” also makes it too big to sit inline, which was the only reason to reach for it.
- Pin domain for a column of them: on independent domains every row peaks and troughs identically, which is how a table of sparklines becomes actively misleading.
- One path, no rendering engine — so a hundred of them in a table cost nothing.