Skip to main content
A scale turns data values into visual values: a number into a position, a text value into a color, a magnitude into a point size. Every aesthetic you map needs one.

Position scales must be declared

Position scales are never created automatically. If you map x or y, add scale.x() / scale.y(). Miss one and the observations get no position on that axis: the compile succeeds and the chart is blank.
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 color scale. With no color scale in the spec, the engine appends scale.color.discrete(), so mapped groups still get the default palette. Under a geom that draws its value as color (geom.tile()), the appended scale is inferred from the mapped column instead, so a numeric column 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 is used.

Inference vs explicit types

Call a position scale bare to infer its 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 needs come before the column’s format. Under a bar layer, scale.x() resolves to a band and scale.y() to a continuous scale; under a tile layer, both resolve to bands. A value axis that bars or areas stand on always holds zero, declared explicitly or not. A domainMin above zero gives way to it, with an INCOMPATIBLE_SCALE_DOMAIN warning.
Name the type to opt out of inference. The scale must still fit the column’s type, or the compile fails with INCOMPATIBLE_TYPE. When the column’s type is wrong, declare its format.
A geom that needs a band still overrules you: a continuous or datetime scale there is replaced by a band, with an UNSUPPORTED_SCALE_TYPE warning. Only its reverse carries over. A bar’s band applies to the whole plot’s shared x scale, sibling line layers included. With numeric x values, geom.line() plus geom.bar() and scale.x.continuous() resolves to a band scale, with UNSUPPORTED_SCALE_TYPE, and the line uses those bands. A line alone keeps its continuous x scale.

Scales by aesthetic

Categorical color

Group colors follow one rule. scale.color.discrete() takes the theme’s palettes.categorical list when a mounted theme has one, else the built-in palette: the multicolor set, or brick tones spread by group count when the geoms touch (stacked bars and areas, pies, tiles). scale.color.discrete({ range }) sets explicit colors in group order and cycles when groups outnumber colors.

Continuous color

Map a numeric column to color and the values run through a ramp instead of a set of hues:
The ramp is the scale’s range when it names one. Otherwise it is the mounted theme’s sequential list, or its diverging list when domainMid is set and falls inside the domain. With no theme list the scale takes the built-in ramp of that class: light brick to dark brick, or blue through neutral gray to brick with the gray at the midpoint.
ReadonlyArray<number | string>
default:"null"
An explicit ramp of two or more stops. A range takes precedence over the theme’s lists.
'rgb' | 'lab' | 'hcl' | 'hsl'
default:"lab"
The space the ramp’s stops are interpolated in. 'lab' is perceptually near-uniform; 'rgb' matches d3’s raw output.
boolean
default:"false"
Flips the ramp so the first stop lands on the high end.
number
default:"null"
Maps this data value, usually 0, to the ramp’s midpoint. The scale becomes diverging only when the value lies strictly inside the domain. Use an odd number of stops with a neutral color in the middle. Even-length diverging ramps still render, blending the two central colors and emitting an EVEN_DIVERGING_RAMP warning.
boolean
Symmetrises the domain about domainMid so equal magnitudes get equal color intensity. Defaults to true once domainMid is set, and has no effect without it.

Identity scales

An identity scale passes data values straight through: { size: 10 } becomes 10px. Use it when the data already holds pixel sizes, opacities or stroke widths.
scale.size.continuous() is not identity: it defaults to a sqrt transform, so a value maps to the point’s area, not its radius. Doubling a value doubles the area, which is what the eye reads.

Continuous scale options

The continuous methods (and .log() / .sqrt(), which are continuous with a transform) accept:
number
Pin the lower bound of the domain; the maximum still follows the data. domainMin: 0 keeps zero on the axis, from the top when the data is all negative.
number
Pin the upper bound of the domain; the minimum still follows the data.
boolean
default:"true"
Extend the domain to round values — [3, 97] becomes [0, 100].
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. A log scale whose domain includes zero falls back to linear, with an INCOMPATIBLE_SCALE_DOMAIN warning.
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 color 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 the data 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 band values, in display order. Omit to take them from the data.
Array<string | number>
Explicit output values — colors, sizes, or dash patterns — aligned to domain.
number
default:"0.1"
The gap between bands as a fraction of the band step, 0–1. Inner padding is padding; outer padding is padding / 2. Under coord.polar every discrete scale’s padding is forced to 0, so pie wedges meet with no gap. geom.tile() defaults its position scales to 0 for the same reason; an explicit padding still applies.
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 value at the top, like a matrix.

Next