Skip to content
Stand with Ukraine flag

Calendar heatmap

  • Widget
  • 13 installs
  • v1.0.0
  • Aug 19, 2026
  • MIT license
  • v4.2+
Calendar heatmap

Telemetry as a grid of days: one cell per calendar day, colored by that day’s value. Switch between week, month and year, and scroll back through earlier periods. The widget drives its own time window and always requests a one-day aggregation interval, so the platform reduces each day server-side and the cell shows exactly that aggregate. Days with no telemetry read as no data rather than as zero, and days still to come are drawn as not yet. Clicking a day hands its bounds to a widget action.

Widget Type
  • Timeseries
Category
  • Charts & Graphs

Calendar heatmap

Shows one telemetry key as a grid of calendar days, each cell colored by that day’s value. Switch between week, month and year, and step back through earlier periods — seasonality, the weekly rhythm and the days a device went quiet are all visible at once, which a line chart over the same span cannot show.

Features

  • Three periods in one widget: week as a row of days, month as a wall calendar, year as a contribution-style grid
  • Always opens on the current period; arrows step back up to 40 periods
  • Drives its own time window and always requests a one-day aggregation interval, so the platform reduces each day server-side
  • Days with no telemetry read as no data, not as zero; days still to come read as not yet
  • Clicking a day hands its bounds to a widget action for drill-down
  • Optional ramp taken from the data key’s own color
  • Configurable fonts, colors, date formats, legend and tooltip
  • No external libraries — the grid is SVG built by the controller, so it works on installations with no internet access
  • Cells scale to the card; a month or week view reads best from about 6×5 grid units, the year view from about 12×3

Data keys

One datasource, one timeseries key. The widget does not expect any particular key name — point it at whatever you measure (energy, pulseCounter, temperature, waterVolume, rainfall, runtimeHours) and every value is bucketed into the calendar day it arrived in.

Datasources1
Data keys1, timeseries
Key nameany
Value typenumeric; strings that parse as numbers are accepted, anything else counts as no reading
Units and decimalstaken from the data key, falling back to the widget-level values

Aggregation type is read from the widget’s own time window and decides what a cell means:

AggregationA cell showsFooter reads
SUMthe day’s totalSum per day
AVGthe day’s meanAverage per day
MIN / MAXthe day’s extremeDaily minimum / maximum
COUNThow many raw readings arrivedReadings per day
NONEraw readings reduced in the browser using How to combine a day’s readingsthat setting’s label

The shipped configuration uses SUM at a one-day interval. Set the aggregation type rather than the widget setting: with aggregation on, the platform has already reduced each day, and a widget setting that disagreed would only mislabel the number.

Cell states

The widget uses four visual states, only one of which is a value:

StateHow it is drawnMeaning
Valuefilled from the color ramp, 2–9 discrete levelsthe day’s aggregate
No dataflat fill in the no readings color, outside the rampthe day passed and nothing arrived
Not yetdashed outline, never filledthe day has not happened yet
Todayaccent outline over its normal fillthe current day, still accumulating, so usually the palest cell

Missing days are excluded from the color scale entirely, so a device that stopped reporting leaves a visible gap rather than a stretch of the lowest color.

Configuration

Settings are grouped by what they affect: Period, Day value, Cell colors, Grid and labels, Label formats, Legend, Tooltip, Period selector and Interaction. Every field carries its own hint in the settings form.

Colors and text in short: the ramp is two ends plus a step count of 2–9 levels, with separate colors for no-data and not-yet days, and it can instead be taken from the data key’s own color. Fonts and colors are set per role — labels, day numbers, legend, tooltip and the period selector — with the accent defaulting to var(--tb-primary-500, #1b6b2f) so an untouched widget follows the platform theme. Day numbers and printed values take a light or dark ink automatically, whichever reads on the cell underneath. Font sizes and label formats are ceilings rather than promises: text shrinks below the configured size, or steps down to a shorter weekday form, instead of overlapping.

The color scale fits the range observed in the shown period and the legend states that range. It is not anchored at zero, because anchoring flattens a week of similar days into a single color — which also means colors are not comparable between two periods. Use a fixed minimum and maximum when that matters.

Title, background and padding come from the widget’s own Appearance settings and are not duplicated here.

Time window

The widget does not read the dashboard time window. It computes the period itself and pushes the matching window down, always asking for a one-day aggregation interval. The shipped configuration therefore sets Use dashboard timewindow off and hides the platform time window picker — leave it that way, two pickers competing for the same subscription is the one setup that will not work.

Days are bucketed in the browser’s local time zone, midnight to midnight, and the window pushed to the platform is aligned to those same midnights so the platform’s daily intervals line up with the cells. Viewing the same dashboard from another time zone shifts values between adjacent cells.

Actions

One action source, Day click, with these parameters:

ParameterMeaning
dayStartTslocal midnight at the start of the clicked day
dayEndTslocal midnight at the start of the next day
valuethe day’s value, or null if there were no readings
readingshow many points landed in that day
periodweek, month or year — which view was clicked

The usual wiring is a dashboard state that drills into the clicked day, with dayStartTs and dayEndTs feeding the target state’s time window. Days in the future are not clickable.

Share Your Widget with the Community

Built a custom widget? Export it as a JSON from ThingsBoard and publish it to the IoT Hub through a simple 4-step wizard (Upload, Listing, Readme, Review & Submit). Share it with thousands of ThingsBoard developers worldwide and get featured in the catalog.