When to use heatmap
Heatmap is the right choice when you want to render a measure across two categorical or temporal dimensions at once. Common uses: day-of-week vs hour-of-day for activity patterns; channel vs region for revenue density; product category vs month for seasonality.
Use grid instead when the audience needs to read the actual numbers and one of the dimensions is high-cardinality. Use scatter when the dimensions are continuous rather than categorical.
Mapping
mapping.x— required. Column-axis field (one tick per distinct value).mapping.y— required. Row-axis field.mapping.value— required. Numeric field that drives the color intensity of each cell.
mapping:
x: hour_of_day
y: day_of_week
value: order_count
chart shortcuts
The chart block is typed and closed.
chart.show_cell_labels— boolean. Renders the cell value inside each cell. Style the labels withchart.labelbelow.chart.cross_filter_emit—"x"or"y". Heatmap has two categorical axes, so you must pick which one's value is emitted on click. Required wheneverchart.cross_filteris not explicitlyfalse; the schema rejects a chart block that has neither.chart.cross_filter— boolean, defaulttrue. Set tofalseto disable click emission entirely; in that casecross_filter_emitis not required.chart.height— pixel height of the viz container.
Color scale
chart.visual_map is an object that controls the color gradient and the optional color-scale legend:
show— boolean. Show the color-scale legend on the side of the chart.orient—"horizontal"|"vertical".min,max— explicit floor / ceiling. Override the data-derived range — useful when several heatmaps on the same dashboard need to share a scale.left/right/top/bottom— pixel number or percent string for placing the color-scale legend.in_range.color— array of hex strings: the gradient stops from low to high. Two stops produce a simple low-to-high gradient.text_style— text styling for the color-scale labels.
chart:
visual_map:
show: true
orient: vertical
in_range:
color: ["#e0f7f4", "#0d9488"]
Cell labels
chart.label styles the per-cell numeric label when chart.show_cell_labels is on:
position,rotate,color,font_size,font_weight.formatter— string template (no callbacks). Use{c}for the value.distance,align,vertical_align,clip.
Cell labels are useful when the heatmap is small and the audience needs the exact number; on dense heatmaps the labels become noise — leave them off and rely on the tooltip.
Legend & tooltip
chart.legend and chart.tooltip share the same shape as on bar. For heatmap the tooltip with trigger: item reveals one cell at a time with both axis values and the cell value.
Axes
chart.x_axis and chart.y_axis share the same shape:
name,name_location,name_gap.axis_label.show,axis_label.rotate,axis_label.interval,axis_label.color,axis_label.font_size,axis_label.font_weight,axis_label.formatter,axis_label.max_chars.
format
format.valueorformat[<value_field_name>]— pattern for cell labels and tooltip values.formatat the root — fallback.
Cross-filter behavior
- Click emits a value only when
chart.cross_filter_emitis set to"x"or"y". - The chosen axis becomes the cross-filter field; the clicked label becomes the value.
- The clicked field must be declared as a parameter in at least one model used by the dashboard.
- Disable per viz with
chart.cross_filter: false(or simply leavecross_filter_emitunset).
See Cross-filtering for the full mechanism.
Worked examples
Activity by hour and day of week:
id: orders_by_day_hour
title: Orders by Day and Hour
query: "models/ec_orders.malloy::by_day_hour"
type: heatmap
mapping:
x: hour_of_day
y: day_of_week
value: order_count
chart:
height: 320
show_cell_labels: true
cross_filter_emit: x
visual_map:
show: true
orient: vertical
in_range:
color: ["#e0f7f4", "#0d9488"]
x_axis:
name: Hour
y_axis:
name: Day
format:
order_count: "#,##0"
published: true
Shared color scale across multiple heatmaps (set explicit min / max):
chart:
visual_map:
show: true
min: 0
max: 5000
in_range:
color: ["#e0f7f4", "#0d9488"]
Common pitfalls
- The chart looks empty even though the query has rows. Make sure both
mapping.xandmapping.ycontain the right fields and the cell value field is non-null. - The color scale is dominated by an outlier. Set
visual_map.maxto clip the scale; values above the cap render at the top color. - Labels are unreadable in dark cells. Set a contrasting
chart.label.color, or turn cell labels off and rely on the tooltip. - Click does nothing. Heatmap requires
chart.cross_filter_emitset to"x"or"y". Without it, no event fires regardless ofcross_filter. - Click should emit both x and y. Heatmap can emit only one axis. If you need both, use a grid.
- Diverging color scale with a midpoint. The exposed
visual_map.in_range.coloris a simple low-to-high gradient; for diverging scales, encode the divergence in the underlying value (e.g. signed delta).