Skip to main content
geom.tile() draws a rectangle that fills its cell on both axes. A bar encodes its value as a length; a tile encodes it as color. The grid comes from a band on x and a band on y, and the value maps to color through a ramp. Use it for a matrix 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 color ramp:
geom.tile() takes no params. The two bands a tile sits in decide its geometry; corner rounding and the hover outline live on style.geom.tile. See What the geom decides for the defaults it brings.

Data shape

A heatmap wants long data: one row per cell, with both coordinates and the value. Matrix data usually arrives wide, with one data column per column of the grid. Pipe transform.reshape in front of the mapping and map the columns it produces:

Color is the encoding

color is required on a tile layer, and its scale is what makes the grid readable. With no color scale in the spec, the engine infers one from the column: a numeric column gets the brand sequential ramp, and a categorical one gets the ordinal palette. The second is the waffle case, where a cell names a category rather than measuring one. Declare the scale to pick the ramp yourself:
For data that crosses zero, use a diverging scheme and pin its neutral stop with domainMid. Without the pin, the neutral color sits at the data’s midpoint, which is rarely zero, and the chart reads as though the break-even point moved:
Setting domainMid also turns on symmetric, so a +8% and a −8% cell get equal color 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 missing combination leaves a hole rather than a cell at the ramp’s low end, so “absent” and “zero” stay distinct. A hole answers no hover either, because the hit-test index drops the same cells the renderer skips:
To show every combination, emit a row for it. The band domains come from the values in the data, so a value that appears nowhere gets no band. Pass 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 read cell by cell, so the number belongs in the cell. Each label centers in its tile and is dropped when the cell is too small to hold it. The text color flips between dark and light to suit the fill under it, 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. The tooltip heading is the cell’s x value, and its one row is the value the color encodes. Highlights work as they do everywhere: a predicate over the post-transform columns. Matched cells stay vivid while the rest step back:
Annotations pinned to an observation address a cell with two values. On most geoms, 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:
An observation anchor resolves to the center of its cell. A pinned number prints the value the ramp encodes, which on a tile is the color value, not a length. A sticker centers on the cell too, so turn the cell labels off where the two would overlap. The kinds that take a panel anchor (text, arrows, shapes) float over the grid in fractions of the plot rect, free of any cell. Give the text a filled box: a ramp runs light to dark under it, so bare text is unreadable at one end or the other. The box comes from the stylesheet, scoped to the annotation by its id:
An annotation anchor joins the two: the arrow in the example runs from the caption’s box to a cell, so neither end is a hand-tuned fraction. The caption’s box is measured in the browser, so the tail moves when the text wraps differently or the plot resizes.

Limits

  • Cartesian only. coord.flip() and coord.polar() reject a tile layer with an UNSUPPORTED_COORD error. A heatmap has no orientation to flip: swap the two mappings instead.
  • identity position only. 'stack', 'dodge' and 'fill' raise UNSUPPORTED_POSITION; none of them means anything without a value axis.
  • Scales — color ramps, schemes and diverging midpoints
  • Transforms — reshaping wide matrix data to long
  • Data labels — the in-cell value labels
  • Highlights — emphasizing the cells a predicate matches
  • Annotations — pinning a callout to one cell