DifferenceChart, explained
Two series, one chart, two-color crossover fill. When the gap between A and B is the story, this is the diagram that makes it readable without arithmetic.
Elijah Meeks·
DifferenceChart is the chart you reach for when the story is the gap between two series, not either series on its own. Plot A and B as overlay lines; fill the area between them with one color where A > B and the other where B > A; let the chart linearly interpolate the crossover points so the fills kiss at zero. The chart's whole job is to make the direction of the difference legible without making the reader do arithmetic.
Why this exists
A two-line chart leaves the difference implicit: the reader has to mentally subtract one line from the other to know which is ahead and by how much. A bar chart of the difference loses the absolute levels. A DifferenceChart does both at once.
The classic uses are:
- Forecast vs actual. Where did we beat the forecast, where did we miss it?
- Year-over-year. Which months are tracking ahead of last year?
- Temperature anomaly. The classic climate-science use case (red where it's warmer than baseline, blue where it's cooler).
- Budget variance. Spent more, spent less, and by how much across the period.
- A/B experiments. Treatment vs control across time, with crossover regions highlighted.
Live demo
Playfair's own 1786 plate is the natural reference: England's imports from and exports to Denmark and Norway over eighty years, with the area between the two lines shaded so the trade balance is the visual headline. Red where imports led; yellow where exports pulled ahead. The crossover sits around 1754.
The technique is older than the modern catalog. Playfair's plate of English imports and exports shades the area between the two lines so the trade balance is the visual headline.William Playfair, The Commercial and Political Atlas, 1786 (public domain).The DifferenceChart below is the same data transcribed onto a Semiotic chart with Playfair's palette maintained showing red when there is a deficit and yellow when exports start to bring money into Britain.
How to read it
- Fill color shows which series is on top at each x: one color for A > B, the other for B > A.
- Crossovers sit at zero-width — the fill collapses to a point exactly where the lines cross, then flips color. The chart linearly interpolates the x of each crossover from adjacent rows, so adjacent segments meet cleanly without jagged seams.
- Overlay lines carry the absolute levels. Turn them off with
showLines={false} if you only need the fill direction.
When to reach for it
Use DifferenceChart when:
- You have exactly two series over the same x.
- The direction of the gap (who's ahead) is the point and not the absolute level of either series.
- Crossover moments are interesting in their own right (when did we tip over from missing the forecast to beating it?).
Reach for something else when:
- You have three or more series. DifferenceChart's two-color fill scheme doesn't generalize.LineChart with
lineBy is the right tool there. - You only need the difference (not the absolute levels). A bar chart of
a - bis more compact. - Each x has many observations and you want their spread. In those cases reach forBoxPlot orViolinPlot or evenCandlestickChart.
Wiring it up
import { DifferenceChart } from "semiotic" <DifferenceChart data={rows} xAccessor="year" seriesAAccessor="a" seriesBAccessor="b" seriesALabel="Imports" seriesBLabel="Exports" showLegend />Defaults: seriesAColor = var(--semiotic-danger),seriesBColor = var(--semiotic-info), both fills at 0.6 opacity, overlay lines on at 1.5 px. All overridable. xScaleType="time" picks upDate x-values; "band" works for categorical x.
Streaming / push mode
Every Semiotic HOC can be driven by either a static data prop OR a forwarded ref that exposes push() / pushMany() / clear(). Push mode is the right reach when rows arrive over time — server-sent events, WebSocket ticks, a setInterval poll, an event stream — and you want the chart to fold them in without unmounting / remounting. DifferenceChart in particular has to recompute its crossover segments every push (it can't precompute them ahead of the data) but the ref-driven internal buffer absorbs that cost without bouncing React state.
Step through the year one month at a time. Watch the chart pick up the new month and recompute the fill between the two series, including the crossover interpolation when the lines cross.
Wiring is identical to a static chart minus the data prop:
const chartRef = useRef() // somewhere a stream feeds the chart... chartRef.current.push({ month: 12, a: 240, b: 295 }) <DifferenceChart ref={chartRef} xAccessor="month" seriesAAccessor="a" seriesBAccessor="b" windowSize={36} // FIFO cap — keep the last 36 rows />Why use push mode here vs setting data={rows} on each update? Two reasons:
- No remount cost. Setting
datato a new array on every tick is a perfectly valid pattern, and Semiotic absorbs it. But push mode skips the React reconciliation for the data prop — the chart reads its internal buffer directly. On long-running streams (minutes of ticks) that's measurable. - Bounded buffer.
windowSize evicts the oldest rows on a FIFO basis so a multi-hour stream doesn't accumulate unbounded memory. Settingdata would mean your code maintains the sliding window manually.