Skip to main content
A scale turns data values into visual values — a number into a pixel position, a category into a color, a magnitude into a point size. Every aesthetic you map needs a scale to interpret it.

Position scales must be declared

Position scales are never created automatically. If you map x or y, you must add scale.x() / scale.y(). Miss one and that axis has no scale, producing NaN positions and a blank chart.
Visual scales (color, size, …) are more forgiving — but declaring them keeps a chart predictable. Two scales appear on their own when the spec implies them:
  • A colour scale. With no color scale in the spec, the engine appends scale.color.palette(), so mapped series still pick up the brand palette. Under a geom whose value rides on colour (geom.tile()), colour is a measure rather than a way of telling series apart, so the appended scale is inferred from the mapped column instead — a numeric one lands on a continuous ramp.
  • A secondary y scale. As soon as any layer sets yScaleType: 'secondary', an inferred ySecondary scale is appended if you didn’t declare one.
Declaring two scales for the same aesthetic isn’t an error — the last one wins.

Inference vs explicit types

Position scales are callable to infer their type from the mapped column’s value format. On x, y and ySecondary:
  • text → discrete (band) scale
  • number / percentage / currency → continuous scale
  • date → datetime scale
A geom’s demands beat the column’s format. Under a bar layer, scale.x() resolves to a band and scale.y() to a continuous scale with zero: true; under a tile layer, both resolve to bands.
Name the type explicitly to opt out of inference, and your options stand whatever the geom would have picked:
A geom whose band is structural still overrules you: a continuous scale there is coerced back to a band, with an UNSUPPORTED_SCALE_TYPE warning.

Scales by aesthetic

Categorical color

scale.color.palette() takes two options. palette names the palette to draw from: overrides recolours individual series without replacing the palette. It’s keyed by group number, 1-indexed, and each entry takes a raw hex or an id to look up in the active custom palette:

Continuous color

Map a numeric column to color and the values run through a ramp instead of a set of hues:
ColorSchemeName
default:"null"
A named colormap, matched case-insensitively. Sequential: viridis, magma, inferno, plasma, cividis, turbo, Blues, Greens, Greys, Oranges, Purples, Reds. Diverging: RdBu, BrBG, PuOr, Spectral — each with a friendly alias (red-blue, brown-teal, purple-orange, spectral).
ReadonlyArray<number | string>
default:"null"
An explicit ramp of two or more stops the value interpolates through. A range supersedes a scheme, and setting both raises a CONFLICTING_COLOR_RAMP warning.
'rgb' | 'lab' | 'hcl' | 'hsl'
default:"lab"
The space a range’s stops are interpolated in. 'lab' is perceptually near-uniform; 'rgb' matches d3’s raw output. Named schemes carry their own interpolation, so this doesn’t apply to them.
number
default:"null"
Pins the ramp’s neutral stop to this data value — usually 0 — rather than the data midpoint. A diverging scheme without it raises a DIVERGING_SCHEME_WITHOUT_MIDPOINT warning.
boolean
Symmetrises the domain about domainMid so equal magnitudes get equal colour intensity. Defaults to true once domainMid is set, and has no effect without it.

Identity scales

An identity scale passes data values straight through as visual values — { size: 10 } becomes 10px, no transformation. Useful when your data already holds pixel sizes or CSS colors.
scale.size.continuous() is not identity: it defaults to a sqrt transform, so a value maps to the marker’s area rather than its radius. Doubling a value doubles the ink, which is what the eye reads.

Continuous scale options

The continuous methods (and .log() / .sqrt(), which are continuous with a transform) accept:
number
Force the lower bound of the domain; the maximum is still computed from the data. domainMin: 0 makes an axis start at zero.
number
Force the upper bound of the domain.
boolean
default:"true"
Extend the domain to round values — [3, 97] becomes [0, 100].
boolean
default:"false"
Include zero in the domain.
boolean
default:"false"
Reverse the direction of the scale.
'linear' | 'log' | 'sqrt'
default:"linear"
A mathematical transform applied to the scale. scale.y.log() is shorthand for transform: 'log'. On size, the default is sqrt.
boolean
Pin out-of-domain values to the ends of the range instead of extrapolating past them. Defaults to false on x / y / ySecondary and true on every other aesthetic.
[number, number]
The output range for the magnitude aesthetics. Ignored on position aesthetics, where the range is the panel. Defaults per aesthetic: size [4, 20], alpha [0.1, 1], strokeWidth [1, 4]. A continuous color scale takes a colour ramp here instead — see Continuous color.

Datetime scale options

scale.x.datetime() and its siblings take epoch-millisecond bounds:
number
Lower bound, in milliseconds since the epoch — Date.parse('2020-01-01').
number
Upper bound, in milliseconds since the epoch.
boolean
default:"false"
Extend the domain to round temporal values. Off by default: rounding to whole months can stretch or clip a series in ways that read as a bug.
boolean
default:"false"
Reverse the direction of the scale.
boolean
default:"false"
Pin out-of-domain instants to the ends of the range.

Discrete scale options

Array<string | number>
The categories, in the order they should appear. Omit to derive them from the data.
Array<string | number>
Explicit output values — colors, sizes, or dash patterns — aligned to domain.
number
default:"0.1"
Gap between bands as a fraction of the band step, 0–1. It’s applied as an inner padding of padding and an outer padding of padding / 2. Under coord.polar every discrete scale’s padding is forced to 0, which is why pie wedges meet with no gap. A geom that tiles the plane — geom.tile() — defaults its position scales to 0 for the same reason; an explicit padding still wins.
boolean
default:"false"
Reverse the band order — the first domain entry maps to the end of the range. Defaults to true on the cross axis of a geom banded on both axes, so a heatmap reads first-category-at-top like a matrix.

Next