# DualSparkline (/docs/charts/dual-sparkline)

DualSparkline answers "how is this series doing against its benchmark?" Exactly two series, ever — three overlapped
lines at 16 px are unreadable (SparkGroup for that). The benchmark whispers: dashed, thinner, neutral — never
distinguished by color alone.

```tsx
<DualSparkline
  data={[12, 13, 12.4, 14, 15.2, 14.8, 16, 17.5, 17, 18.4, 19, 21]}
  compare={[12, 12.4, 12.8, 13.1, 13.6, 14, 14.2, 14.8, 15, 15.4, 15.8, 16]}
  title="Conversion vs market"
  width={220}
  height={28}
/>
```

## Install

```tsx
import { DualSparkline } from "@microcharts/react/dual-sparkline";

<DualSparkline data={ours} compare={market} title="Conversion vs market" />
```

Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.


## When to use it

- **Good for** — metric-vs-benchmark in table cells, actual-vs-plan in KPI cards.
- **Avoid for** — 3+ series (SparkGroup), or different units per series (never dual axes).


## Variants

```tsx
<DualSparkline data={ours} compare={market} label="last" />
<DualSparkline data={ours} compare={market} band={[13, 16]} />
```

## Edge cases

```tsx
// the benchmark is 4 points shorter — it simply ends, never stretched
// to match (stretching would fake a correlation that isn't there)
<DualSparkline data={[12, 13, 12.4, 14, 15.2, 14.8, 16, 17.5]} compare={[12, 12.4, 12.8, 13.1]} />
```

```tsx
<DualSparkline data={[12, null, 12.4, 14, null, 14.8, 16]} compare={[12, 12.4, null, 13.1, 13.6, null, 14.2]} />
```

```tsx
<DualSparkline
  data={[1200, 1300, 1240, 1400, 1520, 1480, 1600]}
  compare={[1200, 1240, 1280, 1310, 1360, 1400, 1420]}
  label="last"
  locale="de-DE"
/>
```

Each series carries its own gaps independently — a `null` in `data` breaks only the primary line at that index, a `null`
in `compare` only the benchmark, and the nearest-x hover still reports both values (or "no data" for whichever side is
missing). With a `locale`, the endpoint label and both hover values follow that locale's grouping.


## Why this default

One shared domain across both series — the entire point is comparability, so there are no dual axes and no per-series
normalization. A shorter benchmark simply ends; stretching it would fake correlation. Coincident endpoints dedupe to one
dot so ink never doubles.

## Accessibility

The accessible name reads both trends and both endpoints — **"Trending up 75% vs benchmark up 33%. Last 21 vs 16."**
Identical series read "Matching benchmark." The interactive entry announces pairs (**"Point 9 of 12: 17 vs 15."**).

The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.

## Props

| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `(number \| null)[]` | The series being judged. |
| `compare` (required) | `(number \| null)[]` | The benchmark — dashed, thinner, neutral. |
| `curve` | `"linear" \| "smooth" \| "step"` | Line shape (default 'linear'). |
| `band` | `[number, number]` | Normal-range band behind both (shared grammar). |
| `label` | `"last" \| "none"` | Endpoint value label for the primary series. |
| `dots` | `"auto" \| "none"` | Endpoint dots on both lines (default "auto"). |
| `seriesStrings` | `SeriesStrings` | i18n strings for the per-series trend clauses. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |

Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
