Skip to content
microcharts
ReferenceCoreDualSparkline

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.

DualSparkline
interactive · 6.1 kB · static · 3.73 kB

Install

Import & usereact/dual-sparkline
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];
Needs package + stylesheetSet up with AI

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

endpoint label vs normal-range band

Edge cases

benchmark ends early
gaps in both series
locale

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

PropTypeNotes
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").
seriesStringsSeriesStringsi18n strings for the per-series trend clauses.
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