Skip to content
cennge-ui
GitHubv0.3.1, changelog

Sparkline

A trend at a glance, beside the number it belongs to: in a stat card, a table row or a line of text.

Open issues, ai-niche-scout

19

down from 31 in 12 weeks

CI minutes in October

2,840

up from 1,700 in March

Commits per week, last 12 weeksLast week
  • cennge-ui48
  • spredo-crypto-screener15
  • ai-niche-scout12
  • payload-cache9
  • playwright-msi-scraper3
  1. The trend is drawn in grey and the latest value is a dot in the accent: switch the accent at the top of the page.
  2. Given a color, the line and the dot both take it, as in the open issues card.
  3. No axes, no readout: each is one picture for a screen reader, named by its aria-label.
Nothing to click: it is drawn whole on the server. Sample data.

Field marks

Import
import { Sparkline } from "cennge-ui/charts";
Size
1.9 KB gzip for this import, its dependencies aside
Pattern
role="img" on an inline <span>, named by aria-label
SSR
Rendered on the server in the Next.js check; hydrates without a warning
Default box
h-8 w-24, inline with text; size it with className
Tests
Unit tests; end-to-end in Chromium, Firefox and WebKit, on desktop and phone viewports: sparkline

When to use#

Use a Sparkline where a number needs its history beside it and there is no room for a chart: a stat card, a row per repository, a sentence. It draws the shape of the values from the lowest to the highest across its box, without axes, so it says which way something went, not by how much. When the reader needs the values, use a LineChart or put the number next to it.

Similar#

  • LineChart has axes, a readout and keyboard access to every value.
  • AreaChart with its axes and grid turned off fits in a card too, and keeps the readout.
  • Progress is one value against a maximum, not a trend.

In a stat card#

variant="area" washes the area under the line in its colour. Give the box its size with className; here it takes the card's width. The numbers on this page are sample data.

CI runs, last 12 weeks

3,257

stat-card.tsx
import { Card, CardBody } from "cennge-ui";
import { Sparkline } from "cennge-ui/charts";

const runs = [212, 198, 240, 251, 236, 270, 288, 276, 301, 322, 315, 348];
const total = runs.reduce((sum, week) => sum + week, 0);

export default function CiRunsCard() {
  return (
    <Card variant="flat" className="w-full max-w-64">
      <CardBody>
        <p className="text-sm text-muted">CI runs, last 12 weeks</p>
        <p className="text-2xl font-semibold tabular-nums">{total.toLocaleString("en-US")}</p>
        <Sparkline
          aria-label="CI runs per week over 12 weeks, up from 212 to 348"
          data={runs}
          variant="area"
          curve="smooth"
          className="mt-2 h-12 w-full"
        />
      </CardBody>
    </Card>
  );
}

In a line of text#

The Sparkline is an inline <span>, set on the middle of the line, so it sits in running text. Keep it about as tall as the text.

payload-cache answered 1.2 million requests last week and 1.1 million the week before.

inline.tsx
import { Sparkline } from "cennge-ui/charts";

// Millions of requests a week, the oldest first.
const requests = [0.9, 0.95, 0.93, 1.02, 1.08, 1.05, 1.14, 1.2];

export default function InlineTrend() {
  return (
    <p className="max-w-md text-sm leading-relaxed">
      payload-cache answered 1.2 million requests last week{" "}
      <Sparkline
        aria-label="Requests per week over 8 weeks, up by a third"
        data={requests}
        className="mx-0.5 h-4 w-14"
      />{" "}
      and 1.1 million the week before.
    </p>
  );
}

Colours that mean something#

By default the trend is grey and the latest value takes the accent. With color both take it: a theme colour that says whether the change is good or bad. The words beside it say the same, so colour is never the only sign.

  • Failed runs a daydown from 92
  • p95 response timeup from 212 ms301 ms
colors.tsx
import { Sparkline } from "cennge-ui/charts";

const trends = [
  {
    name: "Failed runs a day",
    now: "2",
    change: "down from 9",
    data: [9, 7, 8, 6, 5, 6, 4, 3, 4, 2],
    color: "success",
  },
  {
    name: "p95 response time",
    now: "301 ms",
    change: "up from 212 ms",
    data: [212, 205, 219, 231, 228, 246, 263, 270, 287, 301],
    color: "danger",
  },
] as const;

export default function GoodAndBad() {
  return (
    <ul className="flex w-full max-w-md flex-col gap-3">
      {trends.map((trend) => (
        <li key={trend.name} className="flex items-center gap-4 text-sm">
          <span className="min-w-0 flex-1">
            <span className="block font-medium">{trend.name}</span>
            <span className="text-muted">{trend.change}</span>
          </span>
          <Sparkline
            aria-label={`${trend.name}, ${trend.change} to ${trend.now}`}
            data={trend.data}
            color={trend.color}
            className="h-8 w-28"
          />
          <span className="w-16 text-right font-semibold tabular-nums">{trend.now}</span>
        </li>
      ))}
    </ul>
  );
}

Gaps#

null where a value is missing breaks the line; a value alone between two gaps shows as a small dot. showLast={false} drops the dot on the latest value.

Scraper errors a day
gaps.tsx
import { Sparkline } from "cennge-ui/charts";

// Monitoring was down on the 4th and 5th day, and again on the 7th: null, not 0.
const errors = [3, 5, 4, null, null, 6, null, 2, 3, 1];

export default function ErrorsWithGaps() {
  return (
    <div className="flex items-center gap-3 text-sm">
      <span className="text-muted">Scraper errors a day</span>
      <Sparkline
        aria-label="Scraper errors a day over 10 days, with three days missing"
        data={errors}
        className="h-10 w-40"
      />
    </div>
  );
}

Accessibility#

A Sparkline is role="img" and its SVG is hidden, so a screen reader hears the aria-label and nothing else. Write the label as the reader should hear the trend: what it shows, over how long, and which way it went, as "CI runs per week over 12 weeks, up from 212 to 348". When the values themselves matter, give them in text or a table beside it.

Server rendering#

The whole Sparkline is rendered on the server: the line is drawn in a box of its own that stretches to the element, so nothing waits for a measure. The dots are placed in percent, so they stay round however the box is stretched.

Props#

PropTypeDefault
datarequired
readonly (number | null)[]The values, oldest first. A missing one (null) breaks the line.
–
aria-labelrequired
stringWhat it shows and which way it goes, e.g. "Downloads over 12 weeks, up by half".
–
variant
"area""line"A line, or a line with the area under it washed in its colour.
"line"
curve
"linear""smooth"
"linear"
color
ChartColor
the trend in grey and the latest value in the accent; given, both take it
showLast
booleanA dot on the latest value.
true
className
stringSize it here.
h-8 w-24