Skip to content

ExpressiveCSS

Browse documentation

Heatmap chart

A data table drawn as a grid of cells shaded by value, for patterns across two categories such as days and hours.

View as Markdown

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.

Tasks completed per day, last 12 weeks
DayWeek 1Week 2Week 3Week 4Week 5Week 6Week 7Week 8Week 9Week 10Week 11Week 12
Mon253746835946
Tue436285376485
Wed674839562759
Thu348562748367
Fri785947695874
Sat120312013210
Sun012010201021
<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.

Revenue by rep, per month
RepJanFebMarAprMayJun
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.

Tasks completed, last 52 weeks
DayWeek 1Week 2Week 3Week 4Week 5Week 6Week 7Week 8Week 9Week 10Week 11Week 12Week 13Week 14Week 15Week 16Week 17Week 18Week 19Week 20Week 21Week 22Week 23Week 24Week 25Week 26Week 27Week 28Week 29Week 30Week 31Week 32Week 33Week 34Week 35Week 36Week 37Week 38Week 39Week 40Week 41Week 42Week 43Week 44Week 45Week 46Week 47Week 48Week 49Week 50Week 51Week 52
Sun3343122023202443002301012013120310300300402402001330
Mon6497566868967576463365593556644685666883456844486688
Tue77976810795987697978655758846659947898568886957985656
Wed8781076101110107971178656878891067979967106910105117101079869107711
Thu46698755879647106649991069466754964651085599486475871046
Fri9846684555755436947788766584434664884659857659774896
Sat1120203330224030030002443140400320103110032330031000
<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.

Channel score, paid and organic
TypeSearchSocialReferral
Paid814758
Organic685590
<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.

Sales change on last year, in percent
RegionJanFebMarAprMayJun
North-12-504915
South-20-8-23611
East-315101825
<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.

Tasks closed per day
DayWeek 1Week 2Week 3Week 4Week 5Week 6Week 7Week 8Week 9Week 10Week 11Week 12
Mon253746835946
Tue436285376485
Wed674839562759
Thu348562748367
Fri785947695874
Sat120312013210
Sun012010201021
<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
Visits by day and time, last 7 days
DayMorningAfternoonEveningNight
Mon1245388
Tue15504210
Wed18554812
Thu20605214
Fri22586520
Sat30407035
Sun28356832
<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-height32px
--md-comp-heatmap-chart-cell-min-width12px; below it a wide grid scrolls
--md-comp-heatmap-chart-gap3px between cells
--md-comp-heatmap-chart-shape4px, the cell's corner radius
--md-comp-heatmap-chart-colorprimary, the highest value's shade
--md-comp-heatmap-chart-low-colortertiary, the lowest value's shade on a diverging scale
--md-comp-heatmap-chart-empty-colorsurface-container-highest, the lowest value's shade, or the midpoint's
--md-comp-heatmap-chart-grid-coloroutline-variant, a gap's outline

For a selective Sass build, include "heatmap-chart" in $components.

No matching pages

  • Source color

    The seed every generated ramp derives from. Pick one and browse the docs — the whole theme follows. Error does not: it is a fixed hue.

    Default source: Mist Iris.

    ExpressiveCSS presets: Lagoon, Harbor, Breeze, Cobalt, Iris.

    Bold

    Soft

    Mist

    Glow