aw codes icon logo
Docs Hub

Search across every project.

Thermal Switch project

Heatmap visualizations for Filament: calendar activity grids and custom tile layouts.

1.x Current

On this page

Data and scaling

Data formats

Every heatmap takes a plain array of values keyed by cell: dates for the calendar, tile keys for tile layouts, and state abbreviations or names for the U.S. States layout.

// Calendar
['2026-10-01' => 4, '2026-10-02' => 17]

// Tile and U.S. States
['CA' => 1284, 'TX' => 894, 'GA' => 542]

Values may be integers, floats, numeric strings, or null. Numeric strings, which many database drivers return for aggregates, are cast to numbers. Any other value throws an InvalidArgumentException.

A calendar accepts any key that Carbon can parse as a date, so 2026-10-06 13:30:00 counts toward October 6. Values for the same day are added together, and dates outside the visible range are ignored. A key that is not a date throws an InvalidArgumentException. Tile keys work the same way through the layout, as described in Tile.

Zero and missing values

An explicit 0 and a missing value mean different things, and Thermal keeps them apart:

  • Zero — the cell was measured and nothing happened. It is drawn solid gray.
  • Missing — there is no data for the cell, either because its key is absent or because its value is null. It is drawn as an outline, and its description says "no data".

Many queries leave out days or tiles with no activity rather than returning 0 for them. When an absent key should count as a value, set it with fillMissing:

// Widget
protected int | float | null $fillMissing = 0;

// Entry
CalendarHeatmapEntry::make('activity')->fillMissing(0);

Leave it as null, the default, to keep those cells missing.

In this calendar, weekdays report a value or an explicit zero, and weekends report nothing. The zero days are solid gray squares and the missing weekend days are outlined:

A Deployments calendar where weekday rows mix amber cells with solid gray zero cells, and the Saturday and Sunday rows are empty outlined squares A Deployments calendar where weekday rows mix amber cells with solid gray zero cells, and the Saturday and Sunday rows are empty outlined squares

Intensity levels

Each cell is drawn at one of five levels. Level 0 is the baseline for zero, negative and missing values, and levels 1 to 4 are the populated levels. The legend shows all five.

Automatic scaling

By default, Thermal ranks the distinct positive values in the visible data and splits them into quartiles. The largest value always reaches level 4.

Two simpler approaches were evaluated and rejected as the default:

  • Linear of the maximum divides the range from zero to the largest value into four equal bands. A single outlier ruins it: with daily values of 3 to 24 and one launch day of 10,000, every day except the launch lands on level 1.
  • Quantiles of all values handles outliers, but heavily repeated values break it: if 50 of 53 days have a value of 1, every threshold collapses onto 1 and the remaining days can no longer be told apart.

Ranking the distinct values handles both cases, so it is predictable on skewed data, on sparse data and on data with many duplicates. In the outlier example above, the daily values spread across levels 1 to 4, and the launch day sits at level 4 with the busiest ordinary days.

This calendar includes one launch day of 5,000 downloads. The rest of the year still spreads across all four levels:

A Downloads calendar in red with a full range of shades across the year, despite one launch day far above every other value A Downloads calendar in red with a full range of shades across the year, despite one launch day far above every other value

The trade-off is that levels describe rank, not magnitude: level 4 means "among the highest values shown", not "close to the maximum". When the actual size of the values matters, use custom thresholds.

Custom thresholds

Thresholds are the minimum value of each populated level, in ascending order. Return them from getThresholds() on a widget:

protected function getThresholds(): ?array
{
    return [100, 250, 500, 1000];
}

or pass them to thresholds() on an entry:

UsStatesHeatmapEntry::make('leads')->thresholds([100, 250, 500, 1000]);

With these thresholds, 250 to 499 is level 2 and anything from 1,000 up is level 4. Positive values below the first threshold, such as 40, sit at the baseline with zero.

You may give between one and four thresholds. With fewer than four, the upper levels are not used: [10, 20] puts values from 20 up at level 2. Thresholds must be numbers greater than zero in strictly ascending order, or an InvalidArgumentException is thrown.