A <figure class="heatmap-chart"> draws a
<table> as a grid of shaded cells. Each body row
is a row of the grid and each value column is a column, so the grid
has the table's shape.
It takes the same markup as a column chart. The row headers sit beside the grid and the column headers below it. Cells shade from the lowest value to the highest, and a scale under the grid names both ends. A tooltip shows the cell under the pointer. When the chart has focus, the arrow keys move the tooltip round the grid.
Use a heatmap to find patterns across two categories, such as busy days and hours. To compare exact values, use a data table.
| Day | Week 1 | Week 2 | Week 3 | Week 4 | Week 5 | Week 6 | Week 7 | Week 8 | Week 9 | Week 10 | Week 11 | Week 12 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Mon | 2 | 5 | 3 | 7 | 4 | 6 | 8 | 3 | 5 | 9 | 4 | 6 |
| Tue | 4 | 3 | 6 | 2 | 8 | 5 | 3 | 7 | 6 | 4 | 8 | 5 |
| Wed | 6 | 7 | 4 | 8 | 3 | 9 | 5 | 6 | 2 | 7 | 5 | 9 |
| Thu | 3 | 4 | 8 | 5 | 6 | 2 | 7 | 4 | 8 | 3 | 6 | 7 |
| Fri | 7 | 8 | 5 | 9 | 4 | 7 | 6 | 9 | 5 | 8 | 7 | 4 |
| Sat | 1 | 2 | 0 | 3 | 1 | 2 | 0 | 1 | 3 | 2 | 1 | 0 |
| Sun | 0 | 1 | 2 | 0 | 1 | 0 | 2 | 0 | 1 | 0 | 2 | 1 |
<figure class="heatmap-chart">
<figcaption>Tasks completed per day, last 12 weeks</figcaption>
<table>
<thead>
<tr>
<th>Day</th>
<th data-label="">Week 1</th>
<th data-label="">Week 2</th>
<!-- One header per week -->
</tr>
</thead>
<tbody>
<tr><th>Mon</th><td>2</td><td>5</td><!-- … --></tr>
<tr><th>Tue</th><td>4</td><td>3</td><!-- … --></tr>
<!-- One row per day -->
</tbody>
</table>
</figure>
Set an empty data-label to hide a header on the grid,
as the week numbers are here. The table keeps the full header for screen
readers and the tooltip.
Values
Add values to write each cell's text on it. The text is
black or white, whichever reads better on the cell's shade. Give a
formatted cell its number in data-value.
| Rep | Jan | Feb | Mar | Apr | May | Jun |
|---|---|---|---|---|---|---|
| Priya | $45K | $58K | $85K | $41K | $47K | $58K |
| Daniel | $88K | $34K | $45K | $91K | $52K | $40K |
| Amara | $60K | $48K | $65K | $58K | $62K | $70K |
| Leo | $85K | $72K | $78K | $80K | $50K | $98K |
| Wei | $35K | $71K | $30K | $44K | $88K | $90K |
<figure class="heatmap-chart values">
<figcaption>Revenue by rep, per month</figcaption>
<table>
<thead>
<tr><th>Rep</th><th>Jan</th><th>Feb</th><!-- … --></tr>
</thead>
<tbody>
<tr>
<th>Priya</th>
<td data-value="45000">$45K</td>
<td data-value="58000">$58K</td>
<!-- … -->
</tr>
<!-- One row per rep -->
</tbody>
</table>
</figure>
Short labels
A data-label on a header cell replaces its text on the
grid. A column label runs on over the empty labels after it, so a
year of weeks can name each month once. Row labels take up to 40% of
the width and end in an ellipsis past it.
For small cells, set --md-comp-heatmap-chart-row-height
and --md-comp-heatmap-chart-gap lower. A wide grid
narrows its cells to fit, so show fewer columns on a phone. When the
column labels are wider than their columns, the grid keeps every
second or third one whole and hides the rest. To choose which ones
show, leave labels empty yourself.
| Day | Week 1 | Week 2 | Week 3 | Week 4 | Week 5 | Week 6 | Week 7 | Week 8 | Week 9 | Week 10 | Week 11 | Week 12 | Week 13 | Week 14 | Week 15 | Week 16 | Week 17 | Week 18 | Week 19 | Week 20 | Week 21 | Week 22 | Week 23 | Week 24 | Week 25 | Week 26 | Week 27 | Week 28 | Week 29 | Week 30 | Week 31 | Week 32 | Week 33 | Week 34 | Week 35 | Week 36 | Week 37 | Week 38 | Week 39 | Week 40 | Week 41 | Week 42 | Week 43 | Week 44 | Week 45 | Week 46 | Week 47 | Week 48 | Week 49 | Week 50 | Week 51 | Week 52 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Sun | 3 | 3 | 4 | 3 | 1 | 2 | 2 | 0 | 2 | 3 | 2 | 0 | 2 | 4 | 4 | 3 | 0 | 0 | 2 | 3 | 0 | 1 | 0 | 1 | 2 | 0 | 1 | 3 | 1 | 2 | 0 | 3 | 1 | 0 | 3 | 0 | 0 | 3 | 0 | 0 | 4 | 0 | 2 | 4 | 0 | 2 | 0 | 0 | 1 | 3 | 3 | 0 |
| Mon | 6 | 4 | 9 | 7 | 5 | 6 | 6 | 8 | 6 | 8 | 9 | 6 | 7 | 5 | 7 | 6 | 4 | 6 | 3 | 3 | 6 | 5 | 5 | 9 | 3 | 5 | 5 | 6 | 6 | 4 | 4 | 6 | 8 | 5 | 6 | 6 | 6 | 8 | 8 | 3 | 4 | 5 | 6 | 8 | 4 | 4 | 4 | 8 | 6 | 6 | 8 | 8 |
| Tue | 7 | 7 | 9 | 7 | 6 | 8 | 10 | 7 | 9 | 5 | 9 | 8 | 7 | 6 | 9 | 7 | 9 | 7 | 8 | 6 | 5 | 5 | 7 | 5 | 8 | 8 | 4 | 6 | 6 | 5 | 9 | 9 | 4 | 7 | 8 | 9 | 8 | 5 | 6 | 8 | 8 | 8 | 6 | 9 | 5 | 7 | 9 | 8 | 5 | 6 | 5 | 6 |
| Wed | 8 | 7 | 8 | 10 | 7 | 6 | 10 | 11 | 10 | 10 | 7 | 9 | 7 | 11 | 7 | 8 | 6 | 5 | 6 | 8 | 7 | 8 | 8 | 9 | 10 | 6 | 7 | 9 | 7 | 9 | 9 | 6 | 7 | 10 | 6 | 9 | 10 | 10 | 5 | 11 | 7 | 10 | 10 | 7 | 9 | 8 | 6 | 9 | 10 | 7 | 7 | 11 |
| Thu | 4 | 6 | 6 | 9 | 8 | 7 | 5 | 5 | 8 | 7 | 9 | 6 | 4 | 7 | 10 | 6 | 6 | 4 | 9 | 9 | 9 | 10 | 6 | 9 | 4 | 6 | 6 | 7 | 5 | 4 | 9 | 6 | 4 | 6 | 5 | 10 | 8 | 5 | 5 | 9 | 9 | 4 | 8 | 6 | 4 | 7 | 5 | 8 | 7 | 10 | 4 | 6 |
| Fri | 9 | 8 | 4 | 6 | 6 | 8 | 4 | 5 | 5 | 5 | 7 | 5 | 5 | 4 | 3 | 6 | 9 | 4 | 7 | 7 | 8 | 8 | 7 | 6 | 6 | 5 | 8 | 4 | 4 | 3 | 4 | 6 | 6 | 4 | 8 | 8 | 4 | 6 | 5 | 9 | 8 | 5 | 7 | 6 | 5 | 9 | 7 | 7 | 4 | 8 | 9 | 6 |
| Sat | 1 | 1 | 2 | 0 | 2 | 0 | 3 | 3 | 3 | 0 | 2 | 2 | 4 | 0 | 3 | 0 | 0 | 3 | 0 | 0 | 0 | 2 | 4 | 4 | 3 | 1 | 4 | 0 | 4 | 0 | 0 | 3 | 2 | 0 | 1 | 0 | 3 | 1 | 1 | 0 | 0 | 3 | 2 | 3 | 3 | 0 | 0 | 3 | 1 | 0 | 0 | 0 |
<figure class="heatmap-chart" style="
--md-comp-heatmap-chart-row-height: 12px;
--md-comp-heatmap-chart-gap: 2px;
--md-comp-heatmap-chart-shape: 2px">
<figcaption>Tasks completed, last 52 weeks</figcaption>
<table>
<thead>
<tr>
<th>Day</th>
<th data-label="Jan">Week 1</th>
<th data-label="">Week 2</th>
<!-- One header per week -->
</tr>
</thead>
<tbody>
<tr><th data-label="">Sun</th><td>0</td><!-- … --></tr>
<tr><th>Mon</th><td>4</td><!-- … --></tr>
<!-- One row per day -->
</tbody>
</table>
</figure>
Scale
The scale runs from the lowest value to the highest. To fix it, set
data-min and data-max on the chart, such as
0 to 100 for scores. Values past either end take the end's shade.
The scale under the grid still runs from the lowest cell to the
highest, over only the shades they use.
Cells are at least --md-comp-heatmap-chart-cell-min-width
wide, 12px by default. A grid with too many columns for its figure
scrolls its cells sideways instead of narrowing them, and keeps the
row labels in view. The arrow keys scroll the cell they move to into
view.
| Type | Search | Social | Referral |
|---|---|---|---|
| Paid | 81 | 47 | 58 |
| Organic | 68 | 55 | 90 |
<figure class="heatmap-chart values" data-min="0" data-max="100" style="
--md-comp-heatmap-chart-row-height: 56px;
--md-comp-heatmap-chart-gap: 4px;
--md-comp-heatmap-chart-shape: 12px">
<figcaption>Channel score, paid and organic</figcaption>
<table>
<thead>
<tr><th>Type</th><th>Search</th><th>Social</th><th>Referral</th></tr>
</thead>
<tbody>
<tr><th>Paid</th><td>81</td><td>47</td><td>58</td></tr>
<tr><th>Organic</th><td>68</td><td>55</td><td>90</td></tr>
</tbody>
</table>
</figure>
Set data-mid to make the scale diverge from that value,
such as 0 for a change. The midpoint takes the empty color. Values
above it shade towards --md-comp-heatmap-chart-color
and values below towards --md-comp-heatmap-chart-low-color.
Both sides run as far as the end further from the midpoint, so equal
distances get equal shades.
| Region | Jan | Feb | Mar | Apr | May | Jun |
|---|---|---|---|---|---|---|
| North | -12 | -5 | 0 | 4 | 9 | 15 |
| South | -20 | -8 | -2 | 3 | 6 | 11 |
| East | -3 | 1 | 5 | 10 | 18 | 25 |
<figure class="heatmap-chart values" data-mid="0">
<figcaption>Sales change on last year, in percent</figcaption>
<table>
<thead>
<tr><th>Region</th><th>Jan</th><th>Feb</th><!-- … --></tr>
</thead>
<tbody>
<tr><th>North</th><td>-12</td><td>-5</td><!-- … --></tr>
</tbody>
</table>
</figure>
Set data-steps to group the shades into that many
bands. The lowest band takes the empty color, and the scale under
the grid shows a swatch per band. On a diverging scale each side gets that many
bands, and the band nearest the midpoint is the empty color.
| Day | Week 1 | Week 2 | Week 3 | Week 4 | Week 5 | Week 6 | Week 7 | Week 8 | Week 9 | Week 10 | Week 11 | Week 12 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Mon | 2 | 5 | 3 | 7 | 4 | 6 | 8 | 3 | 5 | 9 | 4 | 6 |
| Tue | 4 | 3 | 6 | 2 | 8 | 5 | 3 | 7 | 6 | 4 | 8 | 5 |
| Wed | 6 | 7 | 4 | 8 | 3 | 9 | 5 | 6 | 2 | 7 | 5 | 9 |
| Thu | 3 | 4 | 8 | 5 | 6 | 2 | 7 | 4 | 8 | 3 | 6 | 7 |
| Fri | 7 | 8 | 5 | 9 | 4 | 7 | 6 | 9 | 5 | 8 | 7 | 4 |
| Sat | 1 | 2 | 0 | 3 | 1 | 2 | 0 | 1 | 3 | 2 | 1 | 0 |
| Sun | 0 | 1 | 2 | 0 | 1 | 0 | 2 | 0 | 1 | 0 | 2 | 1 |
<figure class="heatmap-chart" data-steps="4">
<figcaption>Tasks closed per day</figcaption>
<table>
<thead>
<tr><th>Day</th><th>Week 1</th><th>Week 2</th><!-- … --></tr>
</thead>
<tbody>
<tr><th>Mon</th><td>2</td><td>5</td><!-- … --></tr>
</tbody>
</table>
</figure>
Chart cards
Put a chart in a card with the figures it explains. The card adds no chart styles, so size the chart with its tokens.
- Weekly visits
- 1,002
- Busiest on Saturday evening
| Day | Morning | Afternoon | Evening | Night |
|---|---|---|---|---|
| Mon | 12 | 45 | 38 | 8 |
| Tue | 15 | 50 | 42 | 10 |
| Wed | 18 | 55 | 48 | 12 |
| Thu | 20 | 60 | 52 | 14 |
| Fri | 22 | 58 | 65 | 20 |
| Sat | 30 | 40 | 70 | 35 |
| Sun | 28 | 35 | 68 | 32 |
<article class="outlined">
<div class="p-4">
<dl class="stats my-0">
<div>
<dt>Weekly visits</dt>
<dd>1,002</dd>
<dd class="stat-change">Busiest on Saturday evening</dd>
</div>
</dl>
<figure class="heatmap-chart">
<figcaption class="visually-hidden">Visits by day and time, last 7 days</figcaption>
<table><!-- … --></table>
</figure>
</div>
</article>
Values and colors
The chart reads cells the same way a column chart does. A cell's
number is its data-value, or its text without currency
signs, percent signs or grouping commas. Text that is not a single
number, such as "n/a" or "$45K" without a data-value,
is a gap. A gap draws as an outlined cell with no shade, and the
tooltip still shows its text. A chart whose cells are all gaps shows
its table.
Cells mix primary into
surface-container-highest by their place on the scale.
The tooltip swatch takes the cell's shade.
Accessibility
The table is the chart's text alternative. Give it a header row and
a header cell per row, and name the chart with a
<figcaption>. The chart hides the drawn grid,
labels and scale from assistive technology. A data-label
changes only the drawn label.
The plot is one Tab stop, named by the caption. The arrow keys move one cell the way they point. Home and End jump to the ends of the row, and Escape hides the tooltip. Once the tooltip is hidden, Escape goes to a dialog around the chart. On a touch screen, tap a cell or drag across the grid to show a cell. In a right-to-left page the columns run right to left.
The tooltip is a polite live region, so a screen reader hears each
cell's row, column and value as it moves. Shade alone is hard to
tell apart, so use values when exact numbers matter. In
forced colors the cells keep their shades.
JavaScript
AutoInit() starts every .heatmap-chart. To
start one yourself, call HeatmapChart.init. After changing its
table, call update() to redraw it in place. Or put
data-watch on the figure, and it redraws itself. It has
the line chart's options and methods. show() takes a
cell, counted row by row from 0.
const chart = Expressive.HeatmapChart.init(document.querySelector('.heatmap-chart'), { min: 0, max: 100 });
chart.show(13); // highlight the second row's second cell in a 12-column grid; -1 hides the tooltip
chart.update(); // redraw from the table; focus and the highlighted row stay
chart.destroy(); // remove the drawing and show the table again
Tokens
Override these on the .heatmap-chart.
| Token | Default |
|---|---|
--md-comp-heatmap-chart-row-height | 32px |
--md-comp-heatmap-chart-cell-min-width | 12px; below it a wide grid scrolls |
--md-comp-heatmap-chart-gap | 3px between cells |
--md-comp-heatmap-chart-shape | 4px, the cell's corner radius |
--md-comp-heatmap-chart-color | primary, the highest value's shade |
--md-comp-heatmap-chart-low-color | tertiary, the lowest value's shade on a diverging scale |
--md-comp-heatmap-chart-empty-color | surface-container-highest, the lowest value's shade, or the midpoint's |
--md-comp-heatmap-chart-grid-color | outline-variant, a gap's outline |
For a selective Sass build, include "heatmap-chart" in $components.