Skip to content
microcharts
ReferenceDecisionSpreadBand

SpreadBand

SpreadBand fills the signed gap between two series and marks where they last crossed.

encodes
signed area between two lines on ONE shared scale, split at crossings
precision
medium — the filled gap is the read; hover for the exact lead
nodes
≤ 8 (2 band fills + 2 lines + crossing/endpoint dots + label)

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.

SpreadBand
interactive · 5.86 kB · static · 3.38 kB

Install

Import & usereact/spread-band
import { SpreadBand } from "@microcharts/react/spread-band";// pairs — real values under “Sample data” below<SpreadBand data={pairs} seriesLabels={["Organic", "Paid"]} title="Organic vs paid" />
Sample data
const pairs = [  { 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 },];
Needs package + stylesheetSet up with AI

Try it

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

Variants

no gap label
down is good
locale

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

never crosses
identical series
gaps break both lines

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

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: 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

PropTypeNotes
data*{ 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).
animatebooleaninteractiveOpt-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.

Related charts