One git heatmap to rule them all

9 Apr 26

toolspythonvisualisation

GitHub’s contribution heatmap gives me mixed feelings. A year of work, reduced to a grid of green squares. But if your work is spread across multiple git forges—as mine is, between GitHub and two self-hosted GitLabs at ANU (one for teaching, one for research, though the boundary is a bit blurry)—then no single profile page tells the whole story. My GitHub heatmap has gaps that aren’t actually gaps; they’re just weeks where the commits landed somewhere else.

Mostly I was just curious what my developer history since joining GitHub in 2010 (towards the end of my PhD) looked like. So (Claude and) I wrote a script to find out. It pulls contribution data from all three forges and renders a single self-contained SVG covering my entire git history. Here’s what it looks like:

Accurate as of 2026-06-26

This isn’t a live visualisation—the SVG is a static snapshot generated by running the script. It includes data up to the date it was last run.

The whole thing is a single Python file—about 900 lines, with httpx as the only dependency. It uses uv’s inline script metadata so you can run it with uv run contribution_heatmap.py without setting up a virtual environment. GitHub data comes via the GraphQL contributions API, which gives you a nice per-day breakdown by year. GitLab is more work—the events API only retains a year or two of history on most self-hosted instances, so the script scans all your member projects and queries their commit history directly via the repository commits endpoint.

The tiles are week-aggregated rather than daily, because 16 years of daily tiles would produce an unreadably wide image. Each year gets its own row with 53 columns, which keeps things compact while still letting you spot seasonal patterns.

The colour scaling uses global quantile normalisation—non-zero weeks are split into quartiles, so a few massive weeks don’t wash out everything else. The palette matches GitHub’s dark theme, because this site is dark-mode only and I didn’t fancy debugging a light-mode variant.

The SVG is fully self-contained: inline CSS, inline JavaScript for the hover popovers, no external dependencies at view time. Hover over any tile and you get a breakdown by source, a day-of-week mini bar chart for that week, and the top event types. It’s all embedded in foreignObject elements, which is one of those SVG features that feels slightly transgressive but works well in practice.1

Caching is simple but effective. For GitHub, past years get saved as JSON files and only the current (incomplete) year gets refetched. For GitLab, commits are cached per project and only rescanned when a project’s last_activity_at changes. The initial run takes a few minutes (scanning 1,000+ projects), but later runs are near-instant.

Configuration is all via environment variables—GITHUB_USER, GITHUB_TOKEN, GITLAB1_URL, GITLAB1_USER, GITLAB1_TOKEN, and so on for a second GitLab instance. There’s a --dry-run flag that shows what would be fetched without actually hitting any APIs, which was helpful while getting the request counts right.

#Footnotes

  1. foreignObject lets you embed arbitrary HTML inside an SVG. It’s been supported in all major browsers for years, but it still feels like you’re getting away with something.

Cite this post
@online{swift2026oneGitHeatmapToRuleThemAll,
  author = {Ben Swift},
  title = {One git heatmap to rule them all},
  url = {https://benswift.me/blog/2026/04/09/one-git-heatmap-to-rule-them-all/},
  year = {2026},
  month = {04},
  note = {AT-URI: at://did:plc:tevykrhi4kibtsipzci76d76/site.standard.document/2026-04-09-one-git-heatmap-to-rule-them-all},
}