# Sparkline

> Package `@umriss-ui/core`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/core/sparkline/>. Every page in one line: [llms.txt](https://romanhaendler.github.io/umriss-ui/core/llms.txt); every page in full: [llms-full.txt](https://romanhaendler.github.io/umriss-ui/core/llms-full.txt).

A small line of a history, at the height of a line of text, without axes or labels (also called a micro chart). Reach for it beside a value or in a table cell, where the shape of the last hours tells more than the figure alone.

```ts
import { Sparkline } from "@umriss-ui/core";
```

Demo page: https://romanhaendler.github.io/umriss-ui/core/sparkline/

It needs at least two values; with fewer it draws nothing. The line is ink, not meaning: the `accent` tone is for picking out one line among several, never for a verdict.

## Examples

### Beside a value

Pass the values in time order as `data`; the line shows their shape and the figure beside it gives the value.

```tsx
import { Sparkline, Stack, Text } from "@umriss-ui/core";

export default function BesideAValue() {
  return (
    <Stack direction="row" gap={3} align="center">
      <Text as="span" size="sm">
        Parcels delivered per hour
      </Text>
      <Sparkline data={[42, 58, 71, 66, 80, 74, 88]} />
      <Text as="span" size="sm" mono>
        88
      </Text>
    </Stack>
  );
}
```

### Size and tone

`width` and `height` default to a table row; the `accent` tone picks out the one line among several that matters.

```tsx
import { Sparkline, Stack } from "@umriss-ui/core";

const WEEK = [18, 22, 21, 30, 27, 35, 33];

export default function SizeAndTone() {
  return (
    <Stack direction="row" gap={5} align="center" wrap>
      <Sparkline data={WEEK} />
      <Sparkline data={WEEK} width={160} height={40} />
      <Sparkline data={WEEK} width={160} height={40} tone="accent" />
    </Stack>
  );
}
```

### Handle too little history

With fewer than two values there is no shape, so it draws nothing; say in words why the space is empty.

```tsx
import { Sparkline, Stack, Text } from "@umriss-ui/core";

const SERVICES = [
  { name: "Checkout", history: [212, 230, 498, 305, 221] },
  { name: "Payouts (new)", history: [140] },
];

export default function TooLittleHistory() {
  return (
    <Stack gap={2}>
      {SERVICES.map((service) => (
        <Stack key={service.name} direction="row" gap={3} align="center">
          <Text as="span" size="sm" style={{ width: 120 }}>
            {service.name}
          </Text>
          {service.history.length >= 2 ? (
            <Sparkline data={service.history} />
          ) : (
            <Text as="span" size="xs" tone="muted" style={{ width: 96 }}>
              no history yet
            </Text>
          )}
          <Text as="span" size="sm" mono>
            {service.history.at(-1)} ms
          </Text>
        </Stack>
      ))}
    </Stack>
  );
}
```

### Show a history per row

In a table each row gets its line at row height, with the value and the objective in columns of their own.

```tsx
import { Sparkline, Text } from "@umriss-ui/core";

/* Data from the operations world, written out here so the example runs on its own. */
/** A small LCG - the same numbers on every computer. */
function random(seed: number): () => number {
  let s = seed >>> 0;
  return () => {
    s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
    return s / 4294967296;
  };
}

const at = (day: number, hours: number, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
const MINUTE = 60_000;

/** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */
const NOW = at(17, 10, 30);

interface Service {
  id: string;
  name: string;
  team: string;
  /** 1 is customer-facing and pages at night; 3 waits for the morning. */
  tier: 1 | 2 | 3;
  /** The latency objective: the 95th percentile stays below this, in ms. */
  latencySlo: number;
  /** The availability promised for a month, in per cent. */
  availabilityTarget: number;
}

const SERVICES: readonly Service[] = [
  { id: "checkout", name: "Checkout", team: "Payments", tier: 1, latencySlo: 300, availabilityTarget: 99.95 },
  { id: "billing", name: "Billing", team: "Payments", tier: 1, latencySlo: 400, availabilityTarget: 99.9 },
  { id: "sign-in", name: "Sign-in", team: "Identity", tier: 1, latencySlo: 200, availabilityTarget: 99.95 },
  { id: "search", name: "Search", team: "Discovery", tier: 1, latencySlo: 250, availabilityTarget: 99.9 },
  { id: "images", name: "Image service", team: "Discovery", tier: 2, latencySlo: 500, availabilityTarget: 99.5 },
  { id: "notifications", name: "Notifications", team: "Messaging", tier: 2, latencySlo: 800, availabilityTarget: 99.5 },
  { id: "webhooks", name: "Webhooks", team: "Integrations", tier: 2, latencySlo: 1000, availabilityTarget: 99.5 },
  { id: "reports", name: "Reporting", team: "Insights", tier: 3, latencySlo: 2000, availabilityTarget: 99 },
];

interface MetricPoint {
  t: number;
  /** Median and 95th percentile latency, in ms. */
  p50: number;
  p95: number;
  /** Failed requests, in per cent. */
  errorRate: number;
  /** Requests per minute. */
  requests: number;
}

/** Requests per minute at the busiest hour, by tier. */
const VOLUME = { 1: 900, 2: 400, 3: 120 } as const;

/** A service's latency and error rate every five minutes of today, from
    midnight up to now. Busy by day, quiet at night; Checkout's 95th percentile
    climbs well past its objective from 09:40 to 10:10, and its errors with it. */
function metrics(serviceId: string): MetricPoint[] {
  const index = SERVICES.findIndex((one) => one.id === serviceId);
  const service = SERVICES[index];
  if (service === undefined) throw new Error(`No service "${serviceId}".`);
  const r = random(1000 + index * 37);
  const points: MetricPoint[] = [];
  let drift = 0;
  for (let t = at(17, 0); t <= NOW; t += 5 * MINUTE) {
    const hour = (t - at(17, 0)) / (60 * MINUTE);
    const load = 0.35 + 0.65 * Math.max(0, Math.sin((Math.PI * (hour - 5)) / 16));
    drift = 0.8 * drift + (r() - 0.5) * 0.08;
    const base = service.latencySlo * (0.45 + 0.2 * load + drift);
    const surge = serviceId === "checkout" && t >= at(17, 9, 40) && t < at(17, 10, 10) ? 1.9 : 1;
    const p95 = Math.round(base * surge * (1 + r() * 0.08));
    points.push({
      t,
      p50: Math.round(p95 * (0.42 + r() * 0.06)),
      p95,
      errorRate: Math.round((0.05 + r() * 0.12 + (surge > 1 ? 2.4 + r() : 0)) * 100) / 100,
      requests: Math.round(VOLUME[service.tier] * load * (1 + (r() - 0.5) * 0.1)),
    });
  }
  return points;
}

const ROWS = SERVICES.map((service) => {
  const p95 = metrics(service.id)
    .slice(-24)
    .map((point) => point.p95);
  return { service, p95, now: p95.at(-1)! };
});

const CELL = { padding: "var(--u-space-1) var(--u-space-3)", borderBottom: "1px solid var(--u-edge-color)" };

export default function InATable() {
  return (
    <div style={{ overflowX: "auto" }}>
      <table style={{ borderCollapse: "collapse", maxWidth: 560, width: "100%" }}>
        <thead>
          <tr>
            {["Service", "p95, last 2 h", "Now", "Objective"].map((head, i) => (
              <th key={head} style={{ ...CELL, textAlign: i >= 2 ? "right" : "left", whiteSpace: "nowrap" }}>
                <Text as="span" size="xs" tone="muted">
                  {head}
                </Text>
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {ROWS.map(({ service, p95, now }) => (
            <tr key={service.id}>
              <td style={CELL}>
                <Text as="span" size="sm">
                  {service.name}
                </Text>
              </td>
              <td style={CELL}>
                <Sparkline data={p95} />
              </td>
              <td style={{ ...CELL, textAlign: "right", whiteSpace: "nowrap" }}>
                <Text as="span" size="sm" mono>
                  {now} ms
                </Text>
              </td>
              <td style={{ ...CELL, textAlign: "right", whiteSpace: "nowrap" }}>
                <Text as="span" size="sm" mono tone="muted">
                  {service.latencySlo} ms
                </Text>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}
```

## When to use something else

- A figure with its verdict and a history beneath → Stat
- Axes, values, limits or several series → `Line` from @umriss-ui/charts

## API

### `SparklineProps`

| Prop | Type | Default | Description |
|---|---|---|---|
| `data` *required* | `readonly number[]` | — | Values in chronological order; at least two.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/sparkline/#configurator-sparkline), [Beside a value](https://romanhaendler.github.io/umriss-ui/core/sparkline/#beside-a-value), [Size and tone](https://romanhaendler.github.io/umriss-ui/core/sparkline/#size-and-tone) and 4 more |
| `width` | `number \| "fill"` | `96` | Width in pixels; the default is a row width, not a chart width. `"fill"` takes the width of its container, as a tile's history does; the line keeps its stroke and the end point stays round.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/sparkline/#configurator-sparkline), [Size and tone](https://romanhaendler.github.io/umriss-ui/core/sparkline/#size-and-tone), [One above the other (Splitter)](https://romanhaendler.github.io/umriss-ui/core/splitter/#one-above-the-other) and 1 more |
| `height` | `number` | `28` | Height in pixels; the default is as high as a table row. With `width` `"fill"` a stylesheet may set the height instead: the drawing follows its box.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/sparkline/#configurator-sparkline), [Size and tone](https://romanhaendler.github.io/umriss-ui/core/sparkline/#size-and-tone), [One above the other (Splitter)](https://romanhaendler.github.io/umriss-ui/core/splitter/#one-above-the-other) and 1 more |
| `tone` | `"ink" \| "accent"` | `"ink"` | The line is ink, not meaning: `accent` only where exactly one line among several is to be emphasised.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/sparkline/#configurator-sparkline), [Size and tone](https://romanhaendler.github.io/umriss-ui/core/sparkline/#size-and-tone) |
| `ref` | `Ref<HTMLSpanElement>` | — | Receives the component's root element. From `RefProp`.<br>Shown in: [A row that holds still (Sizes)](https://romanhaendler.github.io/umriss-ui/core/sizes/#a-row-that-holds-still), [An anchored surface (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#anchored-surface), [Take the width of the trigger (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#width-from-anchor) and 2 more |

Also takes every attribute of `<span>`.

## Known limits

- No axes, labels, tooltip or points to hover: the value stands beside it.

What umriss deliberately does not build, and why: [ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/).
