Position scales must be declared
Position scales are never created automatically. If you mapx or y, you must add scale.x() / scale.y(). Miss one and that axis has no scale, producing NaN positions and a blank chart.
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
colorscale in the spec, the engine appendsscale.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 inferredySecondaryscale is appended if you didn’t declare one.
Inference vs explicit types
Position scales are callable to infer their type from the mapped column’s value format. Onx, y and ySecondary:
- text → discrete (band) scale
- number / percentage / currency → continuous scale
- date →
datetimescale
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.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 tocolor 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
- Coordinate systems — the plane scales place values into
- Chart types — scales at work in each recipe

