Menu


Heatmap


A grid of days, a column for each week, each day shaded by how much happened, with the months above, a summary and a scale from less to more.

Customize the preview


Show
View

65 days with work

Changes per day, Jul 20 to Oct 18
Week ofMondayTuesdayWednesdayThursdayFridaySaturdaySunday
Jul 20111717171400
Jul 2771817111000
Aug 3121411101460
Aug 1016148111800
Aug 170161071964
Aug 24101117161405
Aug 310121419000
Sep 7111819131547
Sep 1460701000
Sep 21160200006
Sep 281301281003
Oct 51610157060
Oct 1271288930

Options


The heatmap has no choices: it is drawn one way.

Examples


Usage

Half a year of work, a week to a column, each day shaded from none to most, with the number of days with work and the scale under it.

121 days with work

Changes per day, Apr 20 to Oct 18
Week ofMondayTuesdayWednesdayThursdayFridaySaturdaySunday
Apr 20001612904
Apr 2701311132004
May 4001471400
May 111518001200
May 1811131611804
May 2515719181900
Jun 118181914600
Jun 8142018151400
Jun 15001861470
Jun 221515813800
Jun 2918151519906
Jul 614131013000
Jul 1300200000
Jul 20111717171400
Jul 2771817111000
Aug 3121411101460
Aug 1016148111800
Aug 170161071964
Aug 24101117161405
Aug 310121419000
Sep 7111819131547
Sep 1460701000
Sep 21160200006
Sep 281301281003
Oct 51610157060
Oct 1271288930

13 weeks

A quarter: the same parts over 13 weeks.

65 days with work

Changes per day, Jul 20 to Oct 18
Week ofMondayTuesdayWednesdayThursdayFridaySaturdaySunday
Jul 20111717171400
Jul 2771817111000
Aug 3121411101460
Aug 1016148111800
Aug 170161071964
Aug 24101117161405
Aug 310121419000
Sep 7111819131547
Sep 1460701000
Sep 21160200006
Sep 281301281003
Oct 51610157060
Oct 1271288930

Starting midweek

Days before the first value and after the last are left empty, so the weekdays stay in their rows.

60 days with work

Changes per day, Jul 23 to Oct 15
Week ofMondayTuesdayWednesdayThursdayFridaySaturdaySunday
Jul 20171400
Jul 2771817111000
Aug 3121411101460
Aug 1016148111800
Aug 170161071964
Aug 24101117161405
Aug 310121419000
Sep 7111819131547
Sep 1460701000
Sep 21160200006
Sep 281301281003
Oct 51610157060
Oct 1271288

Without the legend

The grid alone. A screen reader still hears the summary and reads the table.

65 days with work

Changes per day, Jul 20 to Oct 18
Week ofMondayTuesdayWednesdayThursdayFridaySaturdaySunday
Jul 20111717171400
Jul 2771817111000
Aug 3121411101460
Aug 1016148111800
Aug 170161071964
Aug 24101117161405
Aug 310121419000
Sep 7111819131547
Sep 1460701000
Sep 21160200006
Sep 281301281003
Oct 51610157060
Oct 1271288930

In a narrow space

A narrow layout shows the last 13 weeks and names their months afresh. The table still holds all 26.

121 days with work

Changes per day, Apr 20 to Oct 18, narrow
Week ofMondayTuesdayWednesdayThursdayFridaySaturdaySunday
Apr 20001612904
Apr 2701311132004
May 4001471400
May 111518001200
May 1811131611804
May 2515719181900
Jun 118181914600
Jun 8142018151400
Jun 15001861470
Jun 221515813800
Jun 2918151519906
Jul 614131013000
Jul 1300200000
Jul 20111717171400
Jul 2771817111000
Aug 3121411101460
Aug 1016148111800
Aug 170161071964
Aug 24101117161405
Aug 310121419000
Sep 7111819131547
Sep 1460701000
Sep 21160200006
Sep 281301281003
Oct 51610157060
Oct 1271288930

Guidelines


Purpose

Shows how much happened on each day over a quarter or a half year, a square for each day shaded from less to more, so a reader sees busy weeks, quiet ones and the rhythm of the week at a glance. It differs from a bar chart, which compares totals a reader can read off.

When to use

  • How much a person or a team worked, published or changed, day by day, over 13 or 26 weeks.
  • A pattern across the days of the week, such as quiet weekends.

When not to use

  • When the reader needs exact amounts. Use a bar chart or a table.
  • For values that can be below zero or that go both ways. Use a chart with a diverging scale.
  • For fewer than about eight weeks. A short table or a bar chart says it better.

Usage rules

Do

  • Say what the values are in the label and the title, with their unit.
  • Give a summary that answers the reader's first question.
  • Give heatmaps side by side one highest value, so their shades compare.

Don't

  • Use the heatmap's shades for anything but more and less.
  • Leave out the label: it titles the table a screen reader reads.

65 days with work

Changes per day, do
Week ofMondayTuesdayWednesdayThursdayFridaySaturdaySunday
Jul 20111717171400
Jul 2771817111000
Aug 3121411101460
Aug 1016148111800
Aug 170161071964
Aug 24101117161405
Aug 310121419000
Sep 7111819131547
Sep 1460701000
Sep 21160200006
Sep 281301281003
Oct 51610157060
Oct 1271288930
Do

Give a summary that answers the first question, and say in the label what is counted.

Changes per day, don't
Week ofMondayTuesdayWednesdayThursdayFridaySaturdaySunday
Jul 20111717171400
Jul 2771817111000
Aug 3121411101460
Aug 1016148111800
Aug 170161071964
Aug 24101117161405
Aug 310121419000
Sep 7111819131547
Sep 1460701000
Sep 21160200006
Sep 281301281003
Oct 51610157060
Oct 1271288930
Don't

Without a summary the reader has to count the squares.

Anatomy

  • The block, as wide as its weeks.
  • The chart part: the weekdays part at the left, naming Monday, Wednesday and Friday in weekday parts, then a week part for each week, Monday at the top. Each week holds a month part, naming the month above the week it starts in, and a cell part for each day. A cell's level is a modifier of the part: level 0 for nothing, then levels 1 to 4 in the sequential data colors from light to strong; a blank cell is a day outside the data.
  • The legend part: the summary part at the left, such as "141 days with work", and the scale part at the right, the words less and more around one square of each level.
  • The table part: the same values, a week to a row, for screen readers only.

Behavior

The wrapper shades each day by a quarter of the highest value per level, or of a highest value the page gives, so several heatmaps can share one scale. A week runs Monday to Sunday; days before the first value and after the last are left empty. In a narrow layout the heatmap shows its last 13 weeks, naming the months as if they began there; the table keeps every week. The wrapper writes the month and weekday names and the table's values in English unless locale names another language (or format writes the values), so a page prepared on a server shows the same words once its script runs. Pointing at a day shows the browser's own tooltip, with its date and value; hand-written markup puts the same words in a title on the day. Nothing moves and nothing takes focus.

Accessibility


What it does for you

  • The picture is hidden from screen readers. They read a table instead, titled by the heatmap's label, with a row for each week and a column for each day, holding each day's value.
  • The summary is in words under the grid and read in its place.
  • The shades are never the only way to read a value: the table holds them all. The lightest shades are faint against the page, which is why the table is always there.
  • In high contrast mode the squares keep their shades, so the scale still reads from less to more.
  • Nothing in it takes focus.

What you must do

  • Give a label: it titles the table a screen reader reads.
  • Give the summary the fact that matters, such as how many days had work.
  • In markup written by hand, keep the table and hide the chart part and the scale part.

How to test

  • With a screen reader, the picture is skipped, the table is read by its label with each week and day's value, and the summary is read in words.
  • With the colors removed, the table and the summary still hold every value.
  • In a forced-color mode, the squares keep their shades and the scale reads from less to more.
  • With text at 200% and the page 320 pixels wide, the heatmap shows its last 13 weeks and the legend stays whole.

Source


The files the heatmap is made of. Its folder on GitHub (opens in a new tab) holds them with its wrapper and its document.

Stylesheet
heatmap.css
/* smbk-heatmap — Heatmap. Figma "Heatmap" 7580:375.
 * A column for each week, Monday at the top, each day a small square in its level's shade: the track's faint
 * gray for none, then four steps of the sequential data color from less to more. Monday, Wednesday and Friday
 * are named at the left and each month above the week it starts in. Under the grid, the summary at the left and
 * the scale from less to more at the right. Days outside the data are left empty. The picture is hidden from
 * screen readers, which read the same values in a table kept out of sight. In a narrow layout the heatmap shows
 * its last 13 weeks; the table keeps them all. */

.smbk-heatmap {
  display: flex;
  flex-direction: column;
  gap: var(--smbk-space-stack-sm);
  width: max-content;
  max-width: 100%;
  color: var(--smbk-color-text-subtle);
  font-family: var(--smbk-typography-body-xs-font-family);
  font-size: var(--smbk-typography-body-xs-font-size);
  font-weight: var(--smbk-typography-body-xs-font-weight);
  line-height: var(--smbk-typography-body-xs-line-height);
  letter-spacing: var(--smbk-typography-body-xs-letter-spacing);
  text-transform: var(--smbk-typography-body-xs-text-transform);
}

/* The block frames the words it hides for screen readers, so they stay inside it wherever it is placed: without a
   frame they hang from the page itself and stretch it, past the end of an area that scrolls. No weight, so a parent
   that places the block keeps its say. */
:where(.smbk-heatmap) {
  position: relative;
}

.smbk-heatmap__chart {
  display: flex;
  gap: var(--smbk-space-4);
}

.smbk-heatmap__weekdays,
.smbk-heatmap__week {
  display: flex;
  flex: none;
  flex-direction: column;
  gap: var(--smbk-space-4);
}

.smbk-heatmap__weekdays {
  width: var(--smbk-size-28);
}

/* The month row: as tall as a line of text, a step further from the grid than the days are from each other.
   A name starts over its week and runs on over the next weeks, whose month slots are empty. */
.smbk-heatmap__month {
  display: block;
  width: var(--smbk-size-12);
  height: calc(var(--smbk-typography-body-xs-font-size) * var(--smbk-typography-body-xs-line-height));
  margin-bottom: calc(var(--smbk-space-stack-sm) - var(--smbk-space-4));
  white-space: nowrap;
}

.smbk-heatmap__weekdays .smbk-heatmap__month {
  width: auto;
}

.smbk-heatmap__month-name--narrow {
  display: none;
}

/* A weekday's name is centred on its row of squares, whose height it does not change. */
.smbk-heatmap__weekday {
  display: flex;
  align-items: center;
  height: var(--smbk-size-12);
  white-space: nowrap;
}

.smbk-heatmap__cell {
  display: block;
  flex: none;
  width: var(--smbk-size-12);
  height: var(--smbk-size-12);
  border-radius: var(--smbk-radius-2);
  background: var(--smbk-color-surface-track-subtle);
}

.smbk-heatmap__cell--level-1 { background: var(--smbk-color-data-sequential-2); }
.smbk-heatmap__cell--level-2 { background: var(--smbk-color-data-sequential-3); }
.smbk-heatmap__cell--level-3 { background: var(--smbk-color-data-sequential-4); }
.smbk-heatmap__cell--level-4 { background: var(--smbk-color-data-sequential-5); }

.smbk-heatmap__cell--blank {
  background: none;
}

.smbk-heatmap__legend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--smbk-space-stack-xs) var(--smbk-space-inline-lg);
}

.smbk-heatmap__summary {
  margin: 0;
}

.smbk-heatmap__scale {
  display: flex;
  align-items: center;
  gap: var(--smbk-space-4);
}

/* The table and a summary without its legend, for screen readers only. */
/* .smbk-heatmap__table and .smbk-heatmap__hidden: hidden from view, kept for screen readers and the keyboard, by the shared rule in visually-hidden/visually-hidden.css. */

/* A table takes the room its rows need whatever width and height it is given, so the hidden one is full size, only
   unseen. It hangs from the block's end corner, reaching back and up, the way a page never scrolls: it adds no room
   to the block or to any area that scrolls around it, and stays a table for screen readers. */
.smbk-heatmap__table {
  inset-block-end: 0;
  inset-inline-end: 0;
}

/* A narrow space shows the last 13 weeks, with the months named as if they began there. */
@container style(--smbk-layout: narrow) {
  .smbk-heatmap__week--early,
  .smbk-heatmap__month-name--wide {
    display: none;
  }

  .smbk-heatmap__month-name--narrow {
    display: inline;
  }
}

/* ---- high contrast (forced colors) ---- */
/* The squares keep their shades, so the scale still reads from less to more; the table holds every value. */

@media (forced-colors: active) {
  .smbk-heatmap__cell {
    forced-color-adjust: none;
  }
}
Tokens it reads
Heatmap tokens
Token and CSSValuePreviewFoundation
space.stack.sm--smbk-space-stack-smcomfortable: 8pxcompact: 6pxchanges with densitySpacing
color.text.subtle--smbk-color-text-subtlelight: #4e555edark: #9ba5b1changes with themeColor
typography.body-xs--smbk-typography-body-xs-font-family--smbk-typography-body-xs-font-size--smbk-typography-body-xs-font-weight--smbk-typography-body-xs-line-height--smbk-typography-body-xs-letter-spacing--smbk-typography-body-xs-text-transformfontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 12pxfontWeight: 400lineHeight: 1.5letterSpacing: 0px

The quick brown fox

Typography
space.4--smbk-space-44pxSpacing
size.28--smbk-size-2828pxSize
size.12--smbk-size-1212pxSize
radius.2--smbk-radius-22pxRadius
color.surface.track-subtle--smbk-color-surface-track-subtlelight: #c4cdd8dark: #2d3237changes with themeColor
color.data.sequential.2--smbk-color-data-sequential-2light: default: #7fa0ff · cockpit: #7ebcd0dark: default: #2f4ab8 · cockpit: #1d5e70changes with themeColor
color.data.sequential.3--smbk-color-data-sequential-3light: default: #557bff · cockpit: #389db9dark: default: #3b5ce3 · cockpit: #1d7a93changes with themeColor
color.data.sequential.4--smbk-color-data-sequential-4light: default: #3b5ce3 · cockpit: #027b96dark: default: #557bff · cockpit: #3097b3changes with themeColor
color.data.sequential.5--smbk-color-data-sequential-5light: default: #2f4ab8 · cockpit: #01596ddark: default: #b8ccff · cockpit: #64b8d2changes with themeColor
space.stack.xs--smbk-space-stack-xs4pxSpacing
space.inline.lg--smbk-space-inline-lgcomfortable: 12pxcompact: 8pxchanges with densitySpacing
Manifest
heatmap.manifest.json
{
  "name": "heatmap",
  "block": "smbk-heatmap",
  "summary": "A grid of days, a column for each week, each day shaded by how much happened, with the months above, a summary and a scale from less to more.",
  "adapt": "css",
  "axes": {},
  "flags": {},
  "parts": [
    "chart",
    "weekdays",
    "weekday",
    "week",
    "month",
    "month-name",
    "cell",
    "legend",
    "summary",
    "scale",
    "scale-word",
    "table",
    "hidden"
  ],
  "figmaNodeId": "7580:375"
}