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

Conversion is up 75% and the market it trades against is up 33%. DualSparkline puts both lines in one word-sized frame
so the gap between them is the read. Exactly two series, ever: three overlapped lines at 16 px are unreadable, and
SparkGroup is the chart for that. The benchmark renders dashed, thinner, and neutral, so it is never distinguished by
color alone. Both series share one domain, so there are no dual axes and no per-series normalization.

```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.

## Try it

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

<DualSparkline
  data={ours}
  compare={market}
/>
```

## When to use it

Use it for metric-vs-benchmark in table cells and actual-vs-plan in KPI cards. For 3 or more series use SparkGroup, and
for series in different units use two charts, since this one never draws dual axes.

## Sizing

**channel rows vs market**

```tsx
{channels.map((c) => (
  <DualSparkline key={c.id} data={c.series} compare={market} title={c.name} />
))}
```

**flip the primary — benchmark as the judged line**

```tsx
<DualSparkline data={market} compare={ours} label="last" />
```

## 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. A `null` in `data` breaks only the primary line at that index, a `null` in `compare`
breaks only the benchmark, and the nearest-x hover still reports both values (or "no data" for whichever side is
missing). A benchmark shorter than the primary series ends where its data ends; stretching it to span the frame would
fake a correlation. Endpoints that land on the same point dedupe to one dot. With a `locale`, the endpoint label and
both hover values follow that locale's grouping.

## Four homes

**In a sentence**

```tsx
<p>
  Checkout conversion is outrunning the market{" "}
  <span className="mc-inline">
    <DualSparkline data={ours} compare={market} width={70} height={18} summary={false} />
  </span>{" "}
  — up 75% vs the market's 33%.
</p>
```

**In a table cell**

```tsx
<td>
  <DualSparkline data={ours} compare={market} width={70} height={18} />
</td>
```

**In a KPI card**

```tsx
<div className="kpi">
  <span className="figure">21%</span>
  <span className="unit">plan 16% · +75% vs +33%</span>
  <DualSparkline data={ours} compare={market} label="last" width={130} height={40} />
</div>
```

**In a tab header**

```tsx
<button className="tab">
  Checkout <DualSparkline data={ours} compare={market} width={50} height={14} />
</button>
```

## 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, press `Escape`, or
press outside the chart. 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).
