Docs / Build Workflow

Visualization — heatmap

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 with chart.label below.
  • 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 whenever chart.cross_filter is not explicitly false; the schema rejects a chart block that has neither.
  • chart.cross_filter — boolean, default true. Set to false to disable click emission entirely; in that case cross_filter_emit is 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.value or format[<value_field_name>] — pattern for cell labels and tooltip values.
  • format at the root — fallback.

Cross-filter behavior

  • Click emits a value only when chart.cross_filter_emit is 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 leave cross_filter_emit unset).

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.x and mapping.y contain the right fields and the cell value field is non-null.
  • The color scale is dominated by an outlier. Set visual_map.max to 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_emit set to "x" or "y". Without it, no event fires regardless of cross_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.color is a simple low-to-high gradient; for diverging scales, encode the divergence in the underlying value (e.g. signed delta).