Calendar heatmap (year of days)
type: calendar renders a GitHub-commit-style 7-row x ~53-column grid. Each cell shaded by its value; days with no value render as outlined blanks so missing data reads as missing. Filter the grid Type column and the heatmap re-aggregates.
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 calendar heatmap of a year of days in Svelte 5. type calendar renders a contribution-style grid of seven rows for the days of the week by about 53 columns for the weeks, each cell shaded by its value in calendarValues through the sequential colour scale; days with no value render as outlined blanks so missing data reads as missing, and hovering shows the date and value. Filter the grid's Type column and the year re-aggregates.
type: 'calendar' renders a GitHub-commit-style grid: 7 rows (days of the week) x ~53 columns (weeks of the year). Each cell is shaded by calendarValues[i].value via the sequential color scale; days with no value render as outlined blanks so missing data is visually obvious. Hover any cell for the date + value.
The grid drives the chart: filter by activity type and the year heatmap re-aggregates.
Imports, features and API used
Imports: @svgrid/grid
Table features registered: rowSortingFeature, columnFilteringFeature
Columns: date (Date), type (Type), count (Count)
SvGridApi methods called: api.getDisplayedRows()
Frequently asked questions
What data does the calendar need?
calendarValues, an array of { date, value } entries; the chart lays the year out from the earliest to the latest date and shades each cell.
How are empty days shown?
As outlined cells with no fill, distinct from a zero value, so a gap in the data is visible rather than hidden as light colour.
Can I show more than one year?
The layout is one year of weeks; for several years render one chart per year, which keeps the week columns aligned and readable.
Related documentation
Related articles
- 15 Years of UI Components - The Story Behind jQWidgets and Smart UI - How a team spent 15 years shipping data grids - from jQuery widgets in 2011 to web components to a Svelte 5 native grid - and what actually changed each time.
- 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 (162-chart-calendar.svelte)
<!-- Documented in: docs/help/charts/types.md -->
<script lang="ts">
/**
* 162. Calendar heatmap (year-of-days)
* -------------------------------------
* `type: 'calendar'` renders a GitHub-commit-style grid: 7 rows
* (days of the week) x ~53 columns (weeks of the year). Each cell is
* shaded by `calendarValues[i].value` via the sequential color scale;
* days with no value render as outlined blanks so missing data is
* visually obvious. Hover any cell for the date + value.
*
* The grid drives the chart: filter by activity type and the year
* heatmap re-aggregates.
*/
import {
SvGrid,
SvGridChart,
tableFeatures,
rowSortingFeature,
columnFilteringFeature,
type GridColumns,
type SvGridApi,
type ChartSpec,
} from '@svgrid/grid'
const features = tableFeatures({ rowSortingFeature, columnFilteringFeature })
type Row = { id: number; date: string; type: 'commit' | 'pr' | 'review'; count: number }
// Year of synthetic GitHub activity: workday peaks, weekly cycle, off
// weeks every few months.
let seed = 0xb00ff1
const rnd = () => ((seed = (seed * 1103515245 + 12345) >>> 0) / 0xffffffff)
const TYPES: Row['type'][] = ['commit', 'pr', 'review']
let nid = 0
const rows: Row[] = []
for (let i = 0; i < 365; i += 1) {
const d = new Date(2026, 0, 1 + i)
const wd = d.getDay()
const weekend = wd === 0 || wd === 6
const offWeek = Math.floor(i / 7) % 9 === 0
for (const type of TYPES) {
let count = 0
if (!offWeek) {
count = type === 'commit' ? Math.round(rnd() * (weekend ? 3 : 12))
: type === 'pr' ? Math.round(rnd() * (weekend ? 1 : 4))
: Math.round(rnd() * (weekend ? 1 : 6))
}
if (count > 0) rows.push({ id: nid++, date: d.toISOString().slice(0, 10), type, count })
}
}
const columns: GridColumns<Row> = [
{ field: 'date', header: 'Date', width: 130 },
{ field: 'type', header: 'Type', width: 110 },
{ field: 'count', header: 'Count', width: 90, align: 'right' },
]
let api = $state<SvGridApi<typeof features, Row> | null>(null)
let displayed = $state<Row[]>(rows)
function sync() { displayed = (api?.getDisplayedRows() as Row[]) ?? rows }
const spec = $derived.by<ChartSpec>(() => {
// Aggregate filtered rows by date.
const byDate = new Map<string, number>()
for (const r of displayed) byDate.set(r.date, (byDate.get(r.date) ?? 0) + r.count)
const calendarValues = Array.from(byDate.entries()).map(([date, value]) => ({ date, value }))
return {
type: 'calendar',
categories: [],
series: [],
calendarValues,
calendarStart: '2026-01-01',
calendarEnd: '2026-12-31',
width: 800,
height: 180,
colorScale: 'sequential',
}
})
</script>
<section class="flex flex-col flex-1 min-h-0 gap-3">
<div class="shrink-0 rounded-lg border px-4 py-3" style="border-color: var(--sg-border); background: var(--sg-header-bg);">
<p class="text-sm font-semibold" style="color: var(--sg-fg);">
Calendar heatmap - a year of activity at a glance
</p>
<p class="mt-0.5 text-xs" style="color: var(--sg-muted);">
Filter the Type column (commit / pr / review) and the heatmap re-aggregates. Hover any cell
for the date and count. Off-weeks every 9 weeks render as outlined blanks so missing data
reads as missing, not zero.
</p>
</div>
<div class="flex flex-1 min-h-0 gap-3 flex-col">
<div class="shrink-0 rounded-lg border p-3" style="border-color: var(--sg-border); background: var(--sg-bg);">
<SvGridChart {spec} />
</div>
<div class="flex-1 min-h-0">
<SvGrid responsive={true}
columnResize
data={rows}
columns={columns}
features={features}
sortable
filterable
selectionMode="none"
rowHeight={28}
containerHeight="100%"
fitColumns={true}
onApiReady={(a) => { api = a; sync() }}
onFiltersChange={sync}
onSortingChange={sync}
/>
</div>
</div>
</section>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.