DualSparkline
DualSparkline draws a series and its benchmark on one shared domain in a single word-sized frame.
- encodes
- two line positions on ONE shared scale
- precision
- medium — the gap between the lines is the read; hover for exact pairs
- nodes
- ≤ 8 (2 paths + band + endpoint dots + label)
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.
Install
import { DualSparkline } from "@microcharts/react/dual-sparkline";// ours, market — real values under “Sample data” below<DualSparkline data={ours} compare={market} title="Conversion vs market" />Sample data
const ours = [12, 13, 12.4, 14, 15.2, 14.8, 16, 17.5, 17, 18.4, 19, 21];const market = [12, 12.4, 12.8, 13.1, 13.6, 14, 14.2, 14.8, 15, 15.4, 15.8, 16];Try it
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
Variants
Edge cases
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
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:
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 | Notes |
|---|---|---|
| data* | (number | null)[] | The series being judged. |
| compare* | (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 | interactiveOpt-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.