Sparkline

Free

Micro-chart small enough to live in a table cell, list row or stat card. Four encodings — line, area, bar and win/loss — with optional smoothing, last-point and extreme markers, a dashed reference baseline and a pinnable domain. Pure SVG, zero dependencies.

</>sv-sparkline charttrendmicrometrictablesvg

Complexity tier: Primitive Presents a value: held to semantic markup and contrast, with its right-to-left and print gaps recorded and the right-to-left debt allowed only to shrink.

import { SvSparklineComponent } from '@styloviz/sparkline';

Demo & usage

Basic usage

Usage example — open the gallery for the full interactive demo.

An interactive demo for this component lives in the gallery.

Open live demo

More examples

In a table row

Nothing is measured and no layout is read, so a sparkline per row is safe inside a virtualized table.

example.htmlAngular
<sv-sparkline [data]="row.series" [width]="96" [height]="24" [ariaHidden]="true" />

Win / loss

Drops magnitude and shows only direction — the right encoding for binary outcomes like builds or deploys.

example.htmlAngular
<sv-sparkline [data]="deploys" variant="winloss" [width]="64" [height]="20" />

Comparable rows — the shared-domain mode

A column of sparklines each scaled to itself invites exactly the comparison it cannot support: row A rising 2% and row B rising 900% draw the identical shape, side by side, and nothing on the page says otherwise. Compute one range over every row's series and hand the same tuple to all of them. When you do not, the accessible label says so out loud — "scaled to this series alone, so its height is not comparable" — because a reader who cannot see the scale is being invited to compare heights that do not mean the same thing.

example.htmlAngular
readonly shared = computed(() => svSharedDomain(this.rows().map(r => r.series)));

// <sv-sparkline [data]="row.series" [domain]="shared()" />

// A value outside the shared range is clamped to the edge rather than drawn
// off-canvas (which is what used to happen — the viewBox simply cut the line
// out of the chart with nothing said), and the label reports how many.

A sample that was never collected

Four policies, and every one of them is a claim about data nobody has. gap (the default) breaks the line and is the only one that invents nothing. skip closes up, which silently rewrites the x axis — a Tuesday the collector never ran renders as a continuous trend from Monday to Wednesday. zero asserts the value was zero, which for a rate or a price is a different statement from "not collected", and it drags the whole domain down with it. connect draws a straight line across. Whichever you pick, the accessible label names it and counts the gaps — being told "12 points" when three of them are holes is the reading a sighted reader gets from the break in the line and a screen-reader user did not.

example.htmlAngular
<!-- null, undefined, NaN and Infinity are all "missing". -->
<sv-sparkline [data]="[10, null, 30, 40]" />
<sv-sparkline [data]="[10, null, 30, 40]" missing="connect" />

Target line

A dashed rule at a budget or goal. For bar charts it is also the value bars grow from.

example.htmlAngular
<sv-sparkline [data]="mrr" [baseline]="110" [showLast]="true" tone="primary" />

When to use

  • Adding a trend column to a table without giving up a full chart's worth of space.
  • Showing momentum beside a single number in a stat card or KPI tile.
  • Summarising a series inside a list row, dropdown or tooltip.
  • Showing pass/fail history — builds, deploys, uptime checks — with the win/loss variant.

Async states

  • loading
  • error
  • retry

Inputs

NameTypeDefaultDescription
loadingbooleanfalseTrue while the series is being fetched.
errorSvDataError | string | nullnullLoad failure. Rendered as a muted dash in the same box, not a card.
datareadonly SparklineDatum[][]The series. Bare numbers, or objects carrying an optional label.
domainMinnumber | nullnullPin the bottom of the value range instead of using the series minimum.
domainMaxnumber | nullnullPin the top of the value range instead of using the series maximum.
domainreadonly [number, number] | nullnull`[min, max]` for the whole group — the shared-domain mode for a table column. A COLUMN OF SPARKLINES EACH SCALED TO ITSELF INVITES EXACTLY THE COMPARISON IT CANNOT SUPPORT. Row A rising 2% and row B rising 900% draw the identical shape, side by side, and nothing on the page says otherwise. Compute one range with `svSharedDomain()` over every row's series and hand the same tuple to all of them. Takes precedence over `domainMin` / `domainMax`, which stay for pinning one end. When none of the three is set, the scale is per-series and the accessible label says so — because a reader who cannot see that is being invited to compare heights that do not mean the same thing.
missingSparklineMissing'gap'What to do with a missing sample.
variantSparklineVariant'area'Drawing style.
widthnumber120Width of the drawing box in px.
heightnumber36Height of the drawing box in px.
strokeWidthnumber1.5Line thickness in px.
smoothbooleanfalseRound the line through point midpoints. Ignored by bar variants.
toneSparklineTone'auto'Colour source.
barGapnumber1Gap between bars in px, for `bar` and `winloss`.
fluidbooleanfalseStretch to the width of the container instead of drawing at `width`. `width` still sets the viewBox, so it stays the coordinate space the geometry is computed in — only the rendered size follows the parent. Use this for a chart that spans a fluid card; leave it off in a table cell, where a fixed width keeps columns aligned.
showLastbooleanfalseDot on the most recent point.
showExtremesbooleanfalseDots on the lowest and highest points.
baselinenumber | nullnullDraw a horizontal rule at this value — a target, budget or zero line.
ariaLabelstring | nullnullOverride the generated summary.
ariaHiddenbooleanfalseHide from assistive tech — use when adjacent text already conveys the data.

Outputs

EventPayload typeDescription
retryvoidThe host should re-run the failed load. Nothing here triggers it.

Wire it to your API

This component is headless about transport: it renders what you give it and reports what the user asks for. Nothing below requires @styloviz/data — drive it from whatever you already use.

Template

Bound to the names this component actually exposes.

usage.htmlAngular
<sv-sparkline
  [data]="data.items()"
  [loading]="data.loading()"
  [error]="data.error()"
  (retry)="data.reload()"
/>

Host class

Drive the three async inputs from your own fetch.

host.component.tsTypeScript
// This component reports its state but does not emit a query, so drive
// it from your own fetch and bind the three async inputs.
readonly items = signal<Row[]>([]);
readonly loading = signal(false);
readonly error = signal<SvDataError | null>(null);

async load(): Promise<void> {
  this.loading.set(true);
  this.error.set(null);
  try {
    this.items.set(await this.api.fetchRows());
  } catch (e) {
    this.error.set(toSvDataError(e));
  } finally {
    this.loading.set(false);
  }
}
  • This component does not emit a query, so it renders what you give it. Fetch on your own trigger and bind `data`, `loading` and `error`.
  • Normalize failures with `toSvDataError()` from `@styloviz/core`. It derives `retryable` from the status, so a 404 does not get a "Try again" button that can only fail again.

Full walkthrough: Connecting to a backend.

Customization & theming

  • Colour comes from `currentColor`, so the chart inherits the text colour around it — wrap it in any `text-*` class to theme it, and dark mode needs no extra work.
  • `tone` sets that colour from a semantic token: `auto` (direction of travel), `primary`, `positive`, `warning`, `negative` or `neutral`.
  • `width`, `height`, `strokeWidth` and `barGap` size the drawing box; the viewBox matches, so nothing is scaled or blurred.
  • `fluid` stretches the chart to its container while keeping `width` as the coordinate space — for a chart spanning a card. Leave it off in a table cell, where a fixed width keeps columns aligned.
  • `smooth` rounds the line using quadratic segments through point midpoints, which keeps the curve inside the data envelope rather than overshooting a spike the way a cubic spline would.
  • The geometry is exported (`pointsOf`, `linePathOf`, `areaPathOf`, `barsOf`, `statsOf`, `svSharedDomain`, `toSlots`, `applyMissing`) for building a custom micro-chart on the same maths.
  • `domain` takes a `[min, max]` tuple for the whole group and wins over `domainMin` / `domainMax`, which stay for pinning one end.
  • `missing` chooses the gap policy: `gap` (default), `skip`, `zero` or `connect`. A gap keeps its slot, so the break lands where the sample actually is and every later point stays put.

Best practices

  • Do: Pass one `domain` to every sparkline in a column. `svSharedDomain()` computes it from all the series at once — otherwise each normalises to its own range and a flat series looks as dramatic as a volatile one.
  • Do: Leave `missing` at `gap` unless you can defend the alternative. The other three each assert something about data nobody collected.
  • Don't: Don't reach for `zero` to make a chart look tidy. "Nothing was measured" and "the measurement was zero" are different facts, and only one of them belongs on a revenue chart.
  • Do: Set `tone` explicitly whenever down is good, such as churn, latency or error rate. `auto` only knows the direction of travel, not whether it is welcome.
  • Do: Set `ariaHidden` when the number and change beside the chart already say everything — a second reading of the same data is noise for a screen reader.
  • Don't: Do not use it where the reader needs to recover a value. A sparkline has no axes and no labels; pair it with the figure, or reach for a full chart.
  • Don't: Do not use `bar` for samples over time — bars imply discrete, countable things. Use `line` or `area` for a rate.
  • Don't: Do not push more points than pixels. Past roughly one point per 2px the shape stops being readable; downsample first.

Accessibility

  • The SVG is `role="img"` with a generated summary — for example *"Sparkline, 12 points, trending up 8.7%, from 92 to 100, low 88, high 104"* — so the series is conveyed without sight of the shape.
  • Pass `ariaLabel` to replace that summary with domain wording.
  • Pass `ariaHidden` to remove it from the accessibility tree; the `role` is dropped along with the label, so no role-less `aria-label` is left behind for axe to flag.
  • Colour is never the only carrier: the win/loss variant also positions losses below the axis, and extreme markers differ in fill as well as position.
  • An empty series renders a sized, `aria-hidden` placeholder so a row does not reflow when data arrives.
  • The summary states the scale: either the shared range it was drawn against (with a count of any points clamped to the edge) or, when none was given, that the chart is scaled to its own series and its height is not comparable with its neighbours.
  • Missing samples are counted in the summary along with what was done about them — "3 missing samples, drawn as breaks" — so the hole a sighted reader sees in the line is not invisible to a screen reader.

Feedback on Sparkline

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.