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

The gutter of the chart below reads **+8**: Organic is eight ahead of Paid, and it took that lead at point 3. SpreadBand
fills the signed gap between a subject and its reference and splits the fill at every crossing, so the current lead and
the last flip both render. The reference line is dashed, thinner, and neutral. Direction is carried by the sign of the
fill and by the text, never by color alone. Both series share one domain: no dual axes, no per-series normalization.

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

## Try it

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

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

## When to use it

Use it for lead-vs-reference in KPI cards, and for actual-vs-plan where the flip matters. Three or more series belong in
SparkGroup; unpaired series or different units belong in separate charts, never on dual axes.

## Sizing

**lead vs reference in a cell**

```tsx
{rows.map((r) => (
  <SpreadBand key={r.id} data={r.pairs} seriesLabels={["Us", "Market"]} title={r.name} />
))}
```

**actual vs plan with the gap**

```tsx
<SpreadBand data={pairs} seriesLabels={["Actual", "Plan"]} label="gap" />
```

## 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 instead of inventing a value. Identical series read "The two series are level — no gap." A
subject that never falls behind reports "never crossed." The fill splits exactly at the interpolated crossings, so a
flip lands where the lines cross, even when that falls between two samples.

## Four homes

**In a sentence**

```tsx
<p>
  Organic vs paid{" "}
  <span className="mc-inline">
    <SpreadBand data={pairs} seriesLabels={["Organic", "Paid"]} summary={false} />
  </span>{" "}
  — organic leads by 18% on median.
</p>
```

**In a table cell**

```tsx
<td>
  <SpreadBand data={pairs} seriesLabels={["Organic", "Paid"]} />
</td>
```

**In a KPI card**

```tsx
<div className="kpi">
  <span className="figure">+18%</span>
  <span className="unit">organic vs paid</span>
  <SpreadBand data={pairs} seriesLabels={["Organic", "Paid"]} />
</div>
```

**In a tab header**

```tsx
<button className="tab">
  Organic <SpreadBand data={pairs} seriesLabels={["Organic", "Paid"]} />
</button>
```

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