Calendar heatmap

daily activity

Daily activity over a year: deploys, incidents, commits.

Calendar heatmap — daily activity: rendered example
Rendered by Gnomon from the source below. No edits.

The source

70 lines of D3 JavaScript, and uses only the bundled D3 build. Copy it, or open the template inside Gnomon and render it as it is.

// Calendar heatmap — a year of daily values, one cell per day.
// Weeks run across, weekdays down, which is the layout everyone already
// knows from commit graphs.

// Seeded so the sample looks the same on every render; swap this loop for
// your own [{date, value}] array.
const rng = d3.randomLcg(7);
const start = new Date(Date.UTC(2025, 0, 1));
const end = new Date(Date.UTC(2026, 0, 1));
const days = d3.utcDay.range(start, end).map(date => {
  const weekday = date.getUTCDay();
  const weekend = weekday === 0 || weekday === 6;
  const base = weekend ? 2 : 14;
  return { date, value: Math.round(base * (0.2 + rng() * 1.6)) };
});

const cell = 15;
const gap = 2;
const margin = { top: 46, right: 20, bottom: 20, left: 44 };

const colour = d3.scaleSequential([0, d3.max(days, d => d.value)], d3.interpolateGreens);
const weekOf = date => d3.utcSunday.count(d3.utcYear(date), date);
const boxWidth = margin.left + (weekOf(days[days.length - 1].date) + 1) * (cell + gap) + margin.right;
const boxHeight = margin.top + 7 * (cell + gap) + margin.bottom;

const svg = d3.select(container).append('svg')
    .attr('viewBox', [0, 0, boxWidth, boxHeight])
    .attr('width', boxWidth)
    .attr('height', boxHeight)
    .attr('font-family', 'system-ui, sans-serif')
    .attr('font-size', 10);

svg.append('text')
    .attr('x', margin.left)
    .attr('y', 20)
    .attr('fill', theme.foreground)
    .attr('font-size', 13)
    .attr('font-weight', 600)
    .text('Deployments per day · 2025');

svg.append('g')
    .attr('fill', theme.muted)
  .selectAll('text')
  .data(['Mon', 'Wed', 'Fri'])
  .join('text')
    .attr('x', margin.left - 6)
    .attr('y', (_, i) => margin.top + (i * 2 + 1) * (cell + gap) + cell / 2)
    .attr('dy', '0.35em')
    .attr('text-anchor', 'end')
    .text(d => d);

svg.append('g')
    .attr('fill', theme.muted)
  .selectAll('text')
  .data(d3.utcMonths(start, end))
  .join('text')
    .attr('x', d => margin.left + weekOf(d) * (cell + gap))
    .attr('y', margin.top - 6)
    .text(d3.utcFormat('%b'));

svg.append('g')
  .selectAll('rect')
  .data(days)
  .join('rect')
    .attr('x', d => margin.left + weekOf(d.date) * (cell + gap))
    .attr('y', d => margin.top + d.date.getUTCDay() * (cell + gap))
    .attr('width', cell)
    .attr('height', cell)
    .attr('rx', 2)
    .attr('fill', d => colour(d.value));

Render this offline

This template ships in Gnomon and renders on your machine, with no account and nothing sent to a server. The browser editor is free and needs no install.

Get GnomonOpen the browser editor

Others in D3 visualisations