Heat maps and calendars
Support tickets by hour and weekday as a heat map matrix with three colour ramps (sequential, diverging for a change against last week, custom stops), values in the cells, and cell selection; and a year of days as a calendar heat map, one square per day laid out in weeks with the months labelled and the range pinned.
A live, editable Svelte 5 data grid example from the SvGrid gallery (Charts). See the SvGrid documentation for the full API.
About this example
A heat map matrix where categories are the columns and each series is a row, colorScale sequential, diverging (centred on zero) or custom stops, dataLabels writing the value where there is room and a click that selects the cell; and a calendar of a year of days from calendarValues laid out in weeks with calendarStart and calendarEnd pinning the range. Free, in @svgrid/grid.
Two ways to colour a value in a grid of cells:
- The heat map is a matrix:
categoriesare the columns (hours of the day) and each series is a row (a day of the week), itsvaluesthe cells.colorScalepicks the ramp:'sequential','diverging'(centred on zero, for a change or a difference), or your own stops. Click a cell to select it. - The calendar is a year of days from
calendarValues, one square per day laid out in weeks with the months labelled;calendarStartandcalendarEndpin the range so an empty month still shows. dataLabelswrites the value in the cell where there is room.
Free, in @svgrid/grid.
Imports, features and API used
Imports: @svgrid/grid, ../shared/ChartDataGrid.svelte, ../shared/ViewSwitch.svelte
Frequently asked questions
How do I feed a heat map?
categories are the columns and each series a row, its values the cells. The colour comes from colorScale: sequential, diverging or an array of colour stops.
When should I use the diverging scale?
For values with a meaningful zero, such as a change or a difference: the scale is centred on zero, so gains and losses take opposite colours and their size the intensity.
How do I draw a calendar heat map?
type calendar with calendarValues as { date, value } pairs; calendarStart and calendarEnd pin the range so empty months still show.
Related documentation
Related articles
- SvGrid Cheat Sheet - The One-Page Quick Reference - Dense copy-paste reference for @svgrid/grid - install, column shapes, features, the imperative API, server-side data, custom cells, and theming tokens on one page.
- Conditional Formatting - Color Cells by Their Value - Four rule types, one prop - add heatmaps, data bars, icon sets, and threshold highlights to any SvGrid column without custom cell renderers.
Source code (445-chart-heatmap-calendar.svelte)
<!-- Documented in: docs/help/charts/gallery.md -->
<script lang="ts">
/**
* 445. Heat maps and calendars
* ----------------------------
* Two ways to colour a value in a grid of cells:
*
* - The heat map is a matrix: `categories` are the columns (hours of the
* day) and each series is a row (a day of the week), its `values` the
* cells. `colorScale` picks the ramp: `'sequential'`, `'diverging'`
* (centred on zero, for a change or a difference), or your own stops.
* Click a cell to select it.
* - The calendar is a year of days from `calendarValues`, one square per
* day laid out in weeks with the months labelled; `calendarStart` and
* `calendarEnd` pin the range so an empty month still shows.
* - `dataLabels` writes the value in the cell where there is room.
*
* Free, in @svgrid/grid.
*/
import { SvChart, type ChartSelection, type ChartSpec } from '@svgrid/grid'
import ChartDataGrid from '../shared/ChartDataGrid.svelte'
import ViewSwitch from '../shared/ViewSwitch.svelte'
let seed = 31
const rnd = () => ((seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648)
const HOURS = Array.from({ length: 24 }, (_, h) => `${String(h).padStart(2, '0')}:00`)
const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
// Support tickets per hour: office hours on weekdays, a lunch dip, quiet weekends.
const load = (d: number, h: number) => {
const weekend = d >= 5
const office = h >= 8 && h <= 18 ? 1 : 0.15
const lunch = h === 12 || h === 13 ? 0.6 : 1
return Math.round((weekend ? 6 : 28) * office * lunch * (0.8 + rnd() * 0.4))
}
const thisWeek = DAYS.map((_, d) => HOURS.map((_, h) => load(d, h)))
const lastWeek = DAYS.map((_, d) => HOURS.map((_, h) => load(d, h)))
let scale = $state<'sequential' | 'diverging' | 'custom'>('sequential')
let mode = $state<'volume' | 'change'>('volume')
let labels = $state(false)
let picked = $state<ChartSelection | null>(null)
const heat = $derived<ChartSpec>({
type: 'heatmap',
categories: HOURS,
series: DAYS.map((day, d) => ({
label: day,
values: mode === 'volume' ? thisWeek[d]! : thisWeek[d]!.map((v, h) => v - lastWeek[d]![h]!),
})),
colorScale: scale === 'custom' ? ['#fef3c7', '#f59e0b', '#7c2d12'] : scale,
title: mode === 'volume' ? 'Support tickets by hour' : 'Change against last week',
subtitle: mode === 'volume' ? 'This week, tickets opened per hour' : 'Tickets per hour, this week minus last week',
dataLabels: { show: labels },
height: 300,
})
const days: Array<{ date: string; value: number }> = []
for (let i = 0; i < 365; i += 1) {
const d = new Date(Date.UTC(2026, 0, 1 + i))
const dow = d.getUTCDay()
const base = dow === 0 || dow === 6 ? 40 : 190
const season = 1 + 0.35 * Math.sin(((i - 60) / 365) * Math.PI * 2)
days.push({ date: d.toISOString().slice(0, 10), value: Math.round(base * season * (0.75 + rnd() * 0.5)) })
}
const calendar: ChartSpec = {
type: 'calendar',
categories: [],
series: [],
calendarValues: days,
calendarStart: '2026-01-01',
calendarEnd: '2026-12-31',
colorScale: 'sequential',
title: 'Tickets per day, 2026',
subtitle: 'One square per day; weekends are the quiet rows',
height: 190,
}
// Chart | Grid: the same spec as the chart or as the rows behind it.
let view = $state<'chart' | 'grid'>('chart')
</script>
<section class="wrap">
<header class="chrome">
<ViewSwitch bind:value={view} options={[['chart', 'Chart'], ['grid', 'Grid']]} />
<label class="ctl">
Values
<select bind:value={mode}>
<option value="volume">Tickets this week</option>
<option value="change">Change vs last week</option>
</select>
</label>
<label class="ctl">
Colours
<select bind:value={scale}>
<option value="sequential">Sequential</option>
<option value="diverging">Diverging</option>
<option value="custom">Custom stops</option>
</select>
</label>
<label class="chk"><input type="checkbox" bind:checked={labels} /> Values in the cells</label>
<span class="note">
{picked ? `${picked.series} ${picked.category}: ${picked.value}` : 'A day by hour matrix with three colour ramps, and a year of days as a calendar. Click a cell.'}
</span>
</header>
<div class="pane">
{#if view === 'chart'}
<SvChart spec={heat} legend={false} selectable onSelect={(s) => (picked = s)} autosize />
{:else}
<ChartDataGrid spec={heat} />
{/if}
</div>
<div class="pane">
{#if view === 'chart'}
<SvChart spec={calendar} legend={false} autosize />
{:else}
<ChartDataGrid spec={calendar} />
{/if}
</div>
</section>
<style>
.wrap {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
gap: 10px;
overflow: auto;
}
.chrome {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
flex: none;
}
.note {
font-size: 12px;
color: var(--sg-muted, #64748b);
}
.chk,
.ctl {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 12px;
color: var(--sg-fg, #0f172a);
white-space: nowrap;
}
.ctl select {
font: inherit;
border: 1px solid var(--sg-border, #e2e8f0);
border-radius: 6px;
background: var(--sg-bg, #fff);
color: inherit;
padding: 2px 6px;
}
.pane {
flex: none;
border: 1px solid var(--sg-border, #e2e8f0);
border-radius: 10px;
background: var(--sg-bg, #fff);
padding: 8px 10px;
min-width: 0;
}
</style>More Charts examples
- Chart a selection (context menu) - With integrated charting enabled, the right-click menu gains a Chart selected range item: select a block of cells, right-click, and the chart panel opens scoped to that range - the Excel chart-this gesture, built in. The item appears only when charting is on and is appended to the default context menu automatically.
- Chart view of the grid - The `chart` prop turns the same <SvGrid> into a chart, driven by the grid’s filtered + sorted rows (search + sort flow through). A view of the grid like board and scheduler, but the renderer is free: the grid lazy-loads a built-in view wrapping the standalone SvChart via rowsToChartSpec. Flip Table <-> Chart (bar / line / area) over one source of truth.
- Candlestick / OHLC - Candlestick and OHLC price marks with an ordinal date axis. ChartSeries.ohlc carries the four prices while values keeps the closes, so the CSV export, the screen-reader table and a 10-day moving average all work with no candle-specific code. Toggle the axis: a real time axis opens a gap over every weekend, an ordinal one spaces sessions evenly and still labels them by date. The strip under the plot is the chart brush: drag its window to pan, drag an edge to resize.
- Box plot + error bars - Distribution rather than average: box plots with the 1.5 IQR whisker rule and individual outliers, next to the same data as a bar chart of the means with error bars. two regions with nearly identical means sit side by side in the bars and look nothing alike in the boxes. boxStats() summarises a raw sample, rowsToBoxSpec() does it per group, and ChartSeries.errors annotates any existing mark without changing its type.
- Axes, titles and styling - The chart hands the decisions back: a numeric x axis that spreads payload sizes the way the numbers do, pinned domains with a fixed tick interval and a formatter per axis, grid lines on or off, a shaded budget band, a vertical reference line, per-point markers and colours, a stepped dashed series on the right axis, a title / subtitle / caption, a legend on any side, and a tooltip snippet in single-series mode. Below it, 50,000 monitoring readings decimated to one point per pixel with LTTB or min / max - toggle it off to see the cost.