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: 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.

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

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>

View this example on GitHub

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.