TanStack
Mark Reference

Difference Marks

differenceY compares two numeric y channels along a numeric or temporal x channel. It fills positive and negative lobes and draws both boundary lines. differenceX transposes the same semantics to two numeric x channels along a numeric or temporal y channel.

ts
import { differenceY } from '@tanstack/charts/difference'

differenceY(rows, {
  x: 'date',
  y1: 'forecast',
  y2: 'actual',
  positiveFill: '#16a34a',
  negativeFill: '#dc2626',
})

Both marks are also exported from @tanstack/charts and @tanstack/charts/universal.

Signatures

ts
function differenceY<TDatum>(
  source: Iterable<TDatum>,
  options: DifferenceYOptions<TDatum>,
): ChartMark<DifferenceDatum<TDatum, InferredX>, InferredX, number>

function differenceX<TDatum>(
  source: Iterable<TDatum>,
  options: DifferenceXOptions<TDatum>,
): ChartMark<DifferenceDatum<TDatum, InferredY>, number, InferredY>

For differenceY, y1 is the comparison and y2 is the primary value. A positive lobe means y2 > y1; a negative lobe means y2 < y1. differenceX applies the same rule to x1 and x2.

The independent channel accepts finite numbers or valid Date values. One mark input cannot mix the two kinds. The value channels accept numeric constants or channels. Input order is path order; sort rows before creating the mark when the semantic independent order differs from source order. Set z to compare one pair of lines per first-seen group.

Crossing and gap semantics

At every sign change, the mark maps both boundary segments through the final x and y scales, solves their exact rendered crossing, and inverts that point back to semantic values. The result remains exact with nonlinear log, power, or symlog scales instead of assuming data-space interpolation is affine. Both configured positional scales must support inversion.

The crossing belongs to both adjacent lobes, so the fills meet without overlap or a gap. Consecutive equal values remain part of the neighboring lobe instead of producing one-point areas.

A row with an invalid independent, comparison, or primary value creates the same gap in both areas and both lines. Later valid rows begin new segments. The mark composes ordinary areaY/areaX and lineY/lineX children; it does not emit a case-specific path.

Options

OptionTypeDefaultMeaning
idstringLayer-derivedStable composite mark ID
x / yOrientation-specific ChannelRequiredNumeric or temporal independent value
y1 / x1number | Channel<TDatum, number?>RequiredComparison boundary
y2 / x2number | Channel<TDatum, number?>RequiredPrimary boundary
zChannel<TDatum, ChartKey?>One groupIndependent comparison groups
keyChannel<TDatum, ChartKey>InferredStable raw-row and derived-lobe identity
positiveFillVisualChannel<DifferenceAreaDatum, string> | null#3ca951Positive-lobe paint; null omits the fill
negativeFillVisualChannel<DifferenceAreaDatum, string> | null#4269d0Negative-lobe paint; null omits the fill
fillOpacitynumber0.2Shared fill opacity
positiveFillOpacitynumberfillOpacityPositive-lobe opacity
negativeFillOpacitynumberfillOpacityNegative-lobe opacity
strokeVisualChannel<TDatum, string>currentColorPrimary-line paint
strokeOpacitynumberSVG defaultPrimary-line opacity
strokeWidthnumber2.25Primary-line width
strokeDasharraystringNonePrimary-line dash pattern
comparisonStrokeVisualChannel<TDatum, string>#64748bComparison-line paint
comparisonStrokeOpacitynumberstrokeOpacityComparison-line opacity
comparisonStrokeWidthnumberstrokeWidthComparison-line width
comparisonStrokeDasharraystringNoneComparison-line dash pattern
pointsbooleanfalseDraws points on both boundary lines
statesreadonly ChartMarkState[]NonePrimary-line focus presentation
comparisonStatesreadonly ChartMarkState[]NoneComparison-line focus presentation
motionChartMotionDefinition<DifferenceDatum<...>>NoneMotion over raw boundaries and derived area rows

The orientation determines the exact channel types. Use DifferenceYOptions or DifferenceXOptions when naming an options object separately. The two fill channels receive derived area rows; the line paint and state channels receive the original source rows.

Derived data and lineage

Area children receive DifferenceAreaDatum rows:

ts
interface DifferenceAreaDatum<TDatum, TIndependent> {
  kind: 'difference-area'
  independent: TIndependent
  comparison: number
  primary: number
  sign: 'positive' | 'negative'
  segment: string
  crossing: boolean
  markKey: ChartKey
  source: readonly TDatum[]
  sourceIndexes: readonly number[]
}

type DifferenceDatum<TDatum, TIndependent> =
  TDatum | DifferenceAreaDatum<TDatum, TIndependent>

The public type surface also includes DifferenceIndependent and DifferenceSign.

An original area point retains its source row and index. An interpolated crossing retains both adjacent source rows and indexes. segment and markKey derive from stable group and source-boundary identity, so prepending an unrelated lobe does not rename later geometry.

Interaction

The positive and negative areas are decorative. The comparison and primary lines each contribute interaction points that retain the original source-row identity. Their child mark IDs end in :comparison and :primary, allowing a tooltip or selection handler to distinguish the two values without receiving synthetic crossing rows.

Set either fill to null to render one-sided emphasis. Set both to null to retain the two interactive boundary lines without areas.