# SpreadBand (/docs/charts/spread-band)

SpreadBand answers "which of two series leads, by how much, and since when?" The signed gap between a subject and its
reference is filled and split at every crossing, so who is ahead and when it last flipped both read at a glance. The
reference whispers: dashed, thinner, neutral — direction is carried by the fill sign and the text, never color alone.

```tsx
<SpreadBand
  data={[
    { a: 8, b: 12 },
    { a: 9, b: 12 },
    { a: 11, b: 13 },
    { a: 12, b: 13 },
    { a: 14, b: 13 },
    { a: 15, b: 14 },
    { a: 17, b: 14 },
    { a: 18, b: 14 },
    { a: 20, b: 15 },
    { a: 21, b: 15 },
    { a: 23, b: 16 },
    { a: 24, b: 16 },
  ]}
  seriesLabels={["Organic", "Paid"]}
  title="Organic vs paid"
  width={220}
  height={30}
/>
```

## Install

```tsx
import { SpreadBand } from "@microcharts/react/spread-band";

<SpreadBand data={pairs} seriesLabels={["Organic", "Paid"]} title="Organic vs paid" />
```

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** — lead-vs-reference in KPI cards, actual-vs-plan where the flip matters.
- **Avoid for** — 3+ series (SparkGroup), or unpaired series / different units (never dual axes).


## Variants

```tsx
<SpreadBand
  data={[
    { a: 8, b: 12 }, { a: 12, b: 13 }, { a: 14, b: 13 }, { a: 17, b: 14 },
    { a: 20, b: 15 }, { a: 24, b: 16 },
  ]}
  seriesLabels={["Organic", "Paid"]}
  label="none"
/>
```

```tsx
// positive="down" flips the fill valence — a lower subject reads as the good side
<SpreadBand
  data={[
    { a: 18, b: 12 }, { a: 16, b: 12 }, { a: 14, b: 13 }, { a: 11, b: 13 },
    { a: 9, b: 14 }, { a: 8, b: 14 },
  ]}
  seriesLabels={["Latency", "Budget"]}
  positive="down"
/>
```

```tsx
<SpreadBand
  data={[
    { a: 8000, b: 12000 }, { a: 9000, b: 12000 }, { a: 11000, b: 13000 },
    { a: 12000, b: 13000 }, { a: 14000, b: 13000 }, { a: 15000, b: 14000 },
    { a: 17000, b: 14000 }, { a: 18000, b: 14000 }, { a: 20000, b: 15000 },
    { a: 21000, b: 15000 }, { a: 23000, b: 16000 }, { a: 24000, b: 16000 },
  ]}
  seriesLabels={["Organic", "Paid"]}
  locale="de-DE"
/>
```

The gutter label and the hover readout follow the `locale`'s grouping — the German build reads the current gap as
**+8.000** and the summary as "Organic leads Paid by 8.000; last crossed at point 5."

## Edge cases

```tsx
// the subject leads throughout — one signed band, no crossing dots
<SpreadBand data={[{ a: 10, b: 5 }, { a: 12, b: 6 }, { a: 14, b: 7 }]} seriesLabels={["A", "B"]} />
```

```tsx
// when the two series coincide there is no gap to fill — one line, no reference
<SpreadBand data={[{ a: 12, b: 12 }, { a: 15, b: 15 }, { a: 14, b: 14 }, { a: 18, b: 18 }]} />
```

```tsx
<SpreadBand data={[{ a: 12, b: 10 }, { a: 14, b: null }, { a: 13, b: 12 }, { a: 16, b: 13 }]} seriesLabels={["A", "B"]} />
```

A `null` in either series is a gap in **both** — the gap is undefined where a reading is missing, so the fill and both
lines break at that index rather than inventing a value. Identical series read "The two series are level — no gap." and
a subject that never falls behind reports "never crossed."


## Why this default

One shared domain across both series — comparability is the whole point, so there are no dual axes and no per-series
normalization. The gap is filled and split exactly at the interpolated crossings, so the eye lands on the flip without
hunting. The reference whispers and the leader's sign is stated in text, so direction never rides on color alone.

## Accessibility

The accessible name states the lead, the current gap, and where the lines last crossed — **"Organic leads Paid by 8;
last crossed at point 3."** Identical series read "The two series are level — no gap." The interactive entry announces
the lead at each point (**"Point 12 of 12: Organic +8 over Paid."**).

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) | `{ a: number \| null; b: number \| null }[]` | Paired readings — a is the subject, b the reference. |
| `seriesLabels` | `[string, string]` | Names the two series in the summary and label. |
| `positive` | `"up" \| "down"` | Which lead is the good valence; down flips the fill colors. |
| `label` | `"gap" \| "none"` | Current signed gap in a right gutter (default gap). |
| `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).
