Activity Heatmap
GitHub-style contribution calendar: a year of day cells, one column per week, each tinted by intensity on a five-step accent ramp, with hover and keyboard detail.
<arc-activity-heatmap>Overview
Activity Heatmap is the contribution calendar most developers already know how to read: fifty-two columns of seven cells, one cell per day, tinted deeper as the day gets busier. Month labels run along the top, sparse weekday labels down the side, and a Less to More legend anchors the ramp. Hovering a cell (or focusing the grid and pressing the arrow keys) raises it with the glow and shows its detail (the date and the day's label or value) in a small built-in bubble.
The data property takes one entry per day with activity: an ISO date, a numeric value, and an optional label that replaces the bare number in the hover detail ("7 commits" reads better than "7"). Days without an entry render as empty cells, so sparse data needs no zero-filling. By default the grid ends today and spans back weeks columns; pin end-date to show a fixed window. When end-date is unset and the component renders server-side, the anchor is derived from the newest date in the data instead of the server's clock, so the server output stays a pure function of props. Pin end-date whenever server and client must agree exactly.
Intensity is relative by default: the nonzero values are split into quartiles, and each quartile maps to one step of the accent ramp, so a quiet repository and a busy one both use the full range. When absolute comparison matters (two heatmaps side by side, or a known ceiling), set max and the ramp becomes a linear scale from zero to that value. The ramp itself is composed from --accent-primary-rgb at five alphas, so overriding one token recolors the whole calendar.
To assistive technology the grid is a single image with a computed description (day count, end date, total, and active days) instead of three hundred and sixty-four tab stops. Keyboard users still get per-day detail: the grid takes focus once, up and down move a day, left and right move a week, Home and End jump to the ends, and a live region announces each cell.
Guidelines
When to use
- Use it for daily event counts over months: commits, deploys, workouts, practice sessions
- Give each entry a label ("7 commits"), since the hover detail and screen reader announcement read it out
- Pin end-date when the window is a fixed report ("2025 in review") or when server and client must render identically
- Set max when readers will compare two heatmaps side by side, because quartile ramps are relative and would mislead
- Match week-start to your audience: Sunday is the GitHub convention, Monday the ISO one
- Keep the legend unless the surrounding UI already explains the ramp; it is the only key the colors have
When not to use
- Do not use it for a continuous metric like latency or revenue. That trend is Sparkline's job; the heatmap shows daily density
- Do not use it for per-period pass/fail health. Uptime owns discrete status history with its own color semantics
- Do not use it to display scheduled items on dates. Calendar shows what happens when; the heatmap shows how much happened
- Do not encode more than one measure per cell. One value gets one ramp; two measures need two heatmaps
Features
- One cell per day in week columns, the layout every contribution graph has taught readers
- Five-step intensity ramp composed from `--accent-primary-rgb`, so one token override recolors it all
- Quartile mapping by default, so sparse and busy datasets both use the full range
- Linear scale via `max` when absolute comparison across heatmaps matters
- Sparse `data` is fine: days without an entry render as empty cells
- Month labels along the top and Mon/Wed/Fri weekday labels down the side, from `Intl`
- Hover or arrow-key a cell to raise it with the glow and show its detail bubble
- Grid-semantics keyboard inspection: up/down a day, left/right a week, Home/End the span
- Single tab stop with an `aria-label` summary and a live region for keyboard users
- Sunday or Monday week start via `week-start`; deterministic server rendering documented on `end-date`
- Cell size and gap tunable via `--activity-heatmap-cell` and `--activity-heatmap-gap`
Preview
Usage
Layout and styling work without JavaScript via the HTML/CSS versions. Interactive features like events and state management require the Web Component or a framework wrapper.
<script type="module" src="@arclux/arc-ui"></script>
<arc-activity-heatmap id="api-activity" end-date="2026-12-31" weeks="52"></arc-activity-heatmap>
<script>
const map = document.querySelector('#api-activity');
// One entry per day with activity; empty days need no entry.
map.data = [
{ date: '2026-12-29', value: 3, label: '3 commits' },
{ date: '2026-12-30', value: 11, label: '11 commits' },
{ date: '2026-12-31', value: 6, label: '6 commits' },
];
// Quartiles scale the ramp by default; pin max for an absolute scale:
map.max = 12;
</script>import { ActivityHeatmap } from '@arclux/arc-ui-react';
const year = commits.map((day) => ({
date: day.date, // 'YYYY-MM-DD'
value: day.count,
label: `${day.count} commits`,
}));
export function ContributionGraph() {
return <ActivityHeatmap data={year} weeks={52} weekStart="monday" />;
}<script setup>
import { ActivityHeatmap } from '@arclux/arc-ui-vue';
const year = commits.map((day) => ({
date: day.date,
value: day.count,
label: `${day.count} commits`,
}));
</script>
<template>
<ActivityHeatmap :data="year" :weeks="52" week-start="monday" />
</template><script>
import { ActivityHeatmap } from '@arclux/arc-ui-svelte';
const year = commits.map((day) => ({
date: day.date,
value: day.count,
label: `${day.count} commits`,
}));
</script>
<ActivityHeatmap data={year} weeks={52} weekStart="monday" />import { Component } from '@angular/core';
import { ActivityHeatmap } from '@arclux/arc-ui-angular';
@Component({
imports: [ActivityHeatmap],
template: `
<arc-activity-heatmap [data]="year" weeks="52" week-start="monday" />
`,
})
export class ContributionGraphComponent {
year = commits.map((day) => ({
date: day.date,
value: day.count,
label: `${day.count} commits`,
}));
}import { ActivityHeatmap } from '@arclux/arc-ui-solid';
const year = commits.map((day) => ({
date: day.date,
value: day.count,
label: `${day.count} commits`,
}));
export function ContributionGraph() {
return <ActivityHeatmap data={year} weeks={52} weekStart="monday" />;
}import { ActivityHeatmap } from '@arclux/arc-ui-preact';
const year = commits.map((day) => ({
date: day.date,
value: day.count,
label: `${day.count} commits`,
}));
export function ContributionGraph() {
return <ActivityHeatmap data={year} weeks={52} weekStart="monday" />;
}API
end-datestring''- The newest day shown, as an ISO string (YYYY-MM-DD). Unset: today in the browser; on the server, the newest date in
data(see above). weeksnumber52- How many week columns to render, counting back from the week containing the end date (default 52), at least 1. The last column is truncated after the end date, so the newest cell is always the end date itself.
weekStart'sunday' | 'monday''sunday'- Which day starts each column: "sunday" (default, the GitHub convention) or "monday". An unrecognised value falls back to sunday.
legendbooleantrue- Whether to render the Less→More swatch strip under the grid. Default true; disable from markup with either
no-legendorlegend="false". dataArray<{date: string, value: number, label?: string}>[]- One entry per day with activity: an ISO
date(YYYY-MM-DD), a numericvaluemapped to the intensity ramp, and an optionallabelshown in the hover detail in place of the bare value (e.g. "7 commits"). Days in the rendered span with no entry render as empty cells, so sparse data is fine. Set it from script, a framework binding, or a JSON attribute. maxnumber0- When set, intensity is a linear scale from 0 to this value instead of the default quartile mapping: each nonzero value lands on step 1-4 by
value / max. Values at or abovemaxrender as step 4.
See Also
- UptimeStatus-page uptime history strip: one slim tick per period, colored by status, with hover and keyboard detail. Accepts plain uptime fractions or explicit status objects and renders the computed overall percentage above the track.
- SparklineTiny inline SVG chart for embedding lightweight line or bar visualizations inside tables, stat cards, and dashboards. Renders from a simple comma-separated data string with no external charting dependencies.
- CalendarMonth-view calendar grid for date selection, with min/max constraints, keyboard navigation, and today highlighting.
- ChartAn SVG chart for dashboards with line, area, bar, and donut types. Driven by a series array, with nice-tick scales, a legend, hover crosshair and tooltips, and a visually-hidden data table for assistive technology.