Pass a D3 scale factory when its domain should come from mark channels. Pass a scale instance when the domain is fixed application state. TanStack Charts copies the resolved scale, assigns its responsive pixel range, and uses that copy for marks, ticks, and interaction. A supplied instance is never mutated.
For the architectural boundary and guidance on selecting granular D3 modules, read Scales and D3. This page documents only the TanStack Charts contract around those primitives.
The common path passes the D3 factory directly:
import { scaleLinear, scaleUtc } from 'd3-scale'
const x = {
scale: scaleUtc,
nice: true,
label: 'Date',
}
const y = {
scale: scaleLinear,
nice: true,
label: 'Revenue',
grid: true,
}The chart creates a fresh scale for each layout, derives its domain from every materialized channel bound to that axis, applies nice, and assigns the responsive range. Bar and area baselines contribute zero when their baseline is implicit. Empty channels retain the D3 factory's native domain.
Return a configured scale from a zero-argument factory for options that should be applied before domain inference:
const x = {
scale: () => scaleBand<string>().padding(0.2),
}nice is an axis option because it must run after the inferred domain exists.
Pass a scale instance when the domain is semantic application state:
const y = {
scale: scaleLinear().domain([0, 100]),
}Instances retain:
TanStack Charts owns:
Scale copies make one configured scale safe to reuse across responsive scenes.
interface ChartAxisOptions<TValue extends ChartValue> {
scale: ChartScale | ConfiguredScaleLike<TValue> | ChartScaleFactory<TValue>
nice?: boolean | number
guide?: boolean
ticks?: number
format?: (value: TValue) => string
grid?: boolean
label?: string
reverse?: boolean
tickRotate?: number
labelOffset?: number
}| Option | Default | Meaning |
|---|---|---|
| scale | Required | D3 factory, configured instance, or advanced ChartScale. |
| nice | false | Nice the resolved domain using the responsive or supplied tick count. |
| guide | true for a non-null axis | Renders the axis, ticks, title, and requested grid. |
| ticks | Responsive target | Suggested tick count passed to the scale. It is not a guaranteed count. |
| format | Scale formatter, then string conversion | Formats tick values. Its value type is inferred from the marks. |
| grid | false for x; true for y | Draws grid rules at tick positions. |
| label | None | Axis title. |
| reverse | false | Reverses the responsive pixel range without changing the caller's scale. |
| tickRotate | 0 | Rotates tick labels in degrees. |
| labelOffset | Automatic | Overrides the title's distance from the axis. |
Set an axis to null only when no mark uses that positional scale. To keep the scale while hiding its guide, use guide: false.
Without an explicit ticks, the responsive target is clamp(2, floor(chart.width / 92), 8) for x and clamp(2, floor(chart.height / 48), 7) for y. The configured scale may return a different number of ticks.
When a margin side is omitted, the scene compiler measures:
The DOM host measures the inherited container font and relayouts after web fonts load. Static compilation uses deterministic estimates unless measureText is supplied. Explicit margin sides remain locked, and clip: true prevents clipped mark labels from expanding the plot.
interface ChartTextMeasurer {
(
text: string,
options: {
fontSize: number
fontWeight?: number
anchor: 'start' | 'middle' | 'end'
baseline: 'auto' | 'middle' | 'hanging'
},
): {
x: number
y: number
width: number
height: number
}
}The returned x and y locate the painted glyph box relative to the requested anchor and baseline. Pass the measurer through a host, adapter, runtime render, or createChartScene layout options.
ChartScale is the unchecked extension boundary for a nonstandard positional mapping:
interface ChartScale {
id: string
resolve(context: ChartScaleResolveContext): ResolvedScale
}
type ChartScaleResolver = (context: ChartScaleResolveContext) => ResolvedScaleThe resolver context contains id, all materialized values, the responsive range, the axis options, a target tickCount, and includeZero. It must return a complete ResolvedScale:
interface ResolvedScale {
id: string
type: string
domain: readonly ChartValue[]
map(value: unknown): number
ticks: readonly { value: ChartValue; label: string; position: number }[]
bandwidth: number
}Prefer a configured scale when it can express the mapping. A custom scale owns correct domains, finite mapping, ticks, formatting, bandwidth, and response to the supplied range.
Data-paint marks accept a semantic color channel. On marks that also expose z, grouping remains independent and supplies the color value only when color is omitted. fill and stroke are final paint overrides, so they do not contribute to the scale or legend.
interface ConfiguredColorScaleLike<TValue extends ChartKey, TOutput> {
(value: TValue): TOutput
copy: () => ConfiguredColorScaleLike<TValue, TOutput>
domain?: () => readonly TValue[]
range?: () => readonly TOutput[]
}
interface ChartColorOptions {
scale?: ConfiguredColorScaleLike<any, any> | ChartColorScaleFactory<any, any>
type?: ChartColorScale
domain?: readonly ChartKey[]
range?: readonly string[]
nice?: boolean | number
legend?: ChartColorLegend
}| Option | Default | Meaning |
|---|---|---|
| scale | None | D3 factory with inference or configured instance with a fixed domain |
| type | None | Custom color-scale resolver |
| domain | Observed channel values | Domain hint for factory, built-in, or custom resolution |
| range | See below | Range for a factory, the built-in scale, or a custom resolver |
| nice | false | Nice a factory or configured continuous color scale |
| legend | None | Legend layout and scene renderer shown above the inner chart |
Resolution order:
A color-scale factory is classified by D3 capabilities:
The theme palette is the range default only for the built-in ordinal scale. A D3 factory must receive color.range or return a scale already configured with a non-empty string range or interpolator; bare D3 factories retain numeric or empty defaults that are not valid paint. Multi-stop continuous ranges receive evenly spaced domain stops across the extent. A supplied scale instance retains its domain and range. Outputs are converted to strings. A custom ChartColorScale receives observed values, optional domain and range, and the resolved theme, then returns:
interface ResolvedColorScale {
type: string
kind?: 'categorical' | 'continuous' | 'quantile' | 'quantize' | 'threshold'
domain: readonly ChartKey[]
range: readonly string[]
thresholds?: readonly number[]
map(value: ChartKey | null | undefined): string
}ChartKey is string | number. On the built-in and configured-scale paths, a null color value maps to the first range color or currentColor. A custom ChartColorScale owns its null mapping through ResolvedColorScale.map. A custom stepped scale supplies exact interior legend boundaries with thresholds; D3 quantile, quantize, and threshold scales derive them automatically.
import { colorLegend } from '@tanstack/charts/legend'
colorLegend({
label: 'Package',
itemWidth: 120,
width: 240,
format: (value) => value.toFixed(0),
})interface ColorLegendOptions {
label?: string
itemWidth?: number
width?: number
format?: (value: number) => string
}itemWidth defaults to 110 and is clamped to a minimum of 64. Items wrap to responsive columns for categorical scales. Continuous scales render a sampled ramp. Quantize, quantile, and threshold scales render exact range bins at their resolved thresholds. width and format configure the quantitative forms.
import { colorGradientLegend } from '@tanstack/charts/legend'
colorGradientLegend({
label: 'Density',
steps: 48,
width: 240,
format: (value) => value.toFixed(1),
})interface ColorGradientLegendOptions {
label?: string
steps?: number
width?: number
format?: (value: number) => string
}steps defaults to 32 and is clamped to at least 2. width defaults to 240, uses at least 80 when space permits, and is finally capped by the inner chart width. The legend requires a numeric first and last color-domain value and throws for a nonnumeric domain.
ChartColorLegend separates layout from rendering:
interface ChartColorLegend {
height(itemCount: number, width: number, colors?: ResolvedColorScale): number
render(context: ChartColorLegendContext): SceneNode
}height reserves space before chart bounds are finalized. render receives the resolved colors, chart bounds, theme, and full width. Return one keyed scene node.
The built-in theme is described in Chart spec. Chart gradients are independent SVG resources:
interface ChartLinearGradient {
id: string
x1?: number
y1?: number
x2?: number
y2?: number
stops: readonly {
offset: number
color: string
opacity?: number
}[]
}Coordinates and offsets are normalized from 0 to 1 by the resource-aware renderer. Omitted coordinates form a vertical gradient from bottom to top. Use renderChartSvgWithResources and an idPrefix; see Rendering and export.