geom.tile() draws a rectangle that fills its cell on both axes. Where a bar encodes its value as a length, a tile encodes it as colour: the grid comes from a category on x and a category on y, and the value rides on the color aesthetic through a ramp. That makes it the tool for a matrix you want to read at a glance — cohort retention, revenue by product and region, activity by day and hour.
Basic example
Three columns: one per axis, one for the value. Both position scales are bands, and the value’s scale is a continuous colour ramp:geom.tile() takes no params. A tile’s geometry is entirely decided by the two bands it sits in, so there is no width or radius knob to turn — see What the geom decides for the defaults it brings instead.
Data shape
A heatmap wants long format: one row per cell, carrying both coordinates and the value. Matrix data usually arrives wide instead — one column per column of the grid — so pipetransform.reshape in front of the mapping and map the columns it produces:
Color is the encoding
color is required on a tile layer, and the scale behind it is what makes the grid readable. On a tile the colour is the value, so with no color scale in the spec the engine infers one from the column: a numeric column gets the brand sequential ramp, and only a categorical one falls back to the ordinal palette — the waffle case, where the cells name a category rather than measuring one. Declare the scale to pick the ramp yourself:
domainMid. Without the pin, the neutral colour drifts to the data’s midpoint — which is rarely zero — and the chart reads as though the break-even point moved:
domainMid also turns on symmetric, so a +8% and a −8% cell get equal colour intensity. Continuous color covers the full option set — schemes, explicit ramps and interpolation spaces.
Gaps stay gaps
Only the rows you supply paint a cell. A grid with missing combinations leaves genuine holes rather than filling them with the ramp’s low end, keeping “absent” and “zero” visually distinct — and a hole answers no hover, because the hit-test index drops the same cells the renderer skipped:domain on the scale to force the full set of bands: scale.x.discrete({ domain: ['Q1', 'Q2', 'Q3', 'Q4'] }).
Value labels
Data labels are on by default on a tile — a heatmap is usually read cell by cell, so the number belongs in the cell. Each label centres in its tile, and is dropped when the cell is too small to hold it. The ink flips between dark and light according to the fill it sits on, so labels stay legible at both ends of the ramp. Turn them off for a chart meant to be read as a texture:What the geom decides
A tile carries defaults that a bar or a line wouldn’t want, so a heatmap looks right before you configure anything:Hover, highlights and annotations
Hover hit-tests the cell the cursor is inside — the full band, including the inset around the painted tile, so the whole grid is live. The hovered cell gains an outline, and the tooltip pairs the cell’sx category as its header with a single row for the value the colour encodes.
Highlights work as they do everywhere — a predicate over the post-transform columns, with matched cells staying vivid while the rest step back:
anchorValue plus the group is enough; on a grid, anchorValue names a whole column, so crossValue names the band that picks one cell out of it:
color value, not a length. A sticker centres on the cell too, so turn the cell labels off where the two would sit on top of each other.
The kinds that take a panel anchor — text, arrows, shapes — float over the grid in fractions of the plot rect instead, free of any cell. Give text an opaque background: a ramp runs light to dark under it, so a transparent label is unreadable at one end or the other.
Limits
- Cartesian only.
coord.flip()andcoord.polar()reject a tile layer with anUNSUPPORTED_COORDerror. A heatmap has no orientation to flip: swap the two mappings instead. identityposition only.'stack','dodge'and'fill'raiseUNSUPPORTED_POSITION— none of them mean anything without a value axis.- No paint vocabulary of its own. There is no
style.geom.tiletarget: the fill comes from the colour scale, and the corner rounding and inset are fixed.style.geom({ alpha })still applies, as do thehoveredanddimmedstates.
Related
- Scales — colour ramps, schemes and diverging midpoints
- Transforms — reshaping wide matrix data to long
- Data labels — the in-cell value labels
- Highlights — emphasising the cells a predicate matches
- Annotations — pinning a callout to one cell

