Back to Blog

Building a GitHub Heatmap Into My CV (And the Bugs That Nearly Broke It)

The Idea

The header on my CV page originally just had my name and a subtitle on a plain background. Functional, but boring.

I had the idea to pull my actual GitHub contribution data and render it as a heatmap grid behind the text. Subtle and low opacity, but visible if you look for it.

GitHub’s contribution calendar is only available through the GraphQL API, which requires authentication. Instead, I used the public events endpoint. I pulled three pages of 100 events each, grouped them by date, and mapped them onto a 53 x 7 grid.

It is not a full year of commit counts, but it shows real recent activity, which is enough for the effect.


Bug One: The Grid Was All Grey

After getting the fetch working, every cell rendered the same dark grey. No greens, nothing. It looked like a placeholder.

The issue was in the date mapping. I had written two setDate() calls on the same Date object to calculate where the grid started:

start.setDate(start.getDate() - (WEEKS * DAYS - 1) + (6 - today.getDay()));
start.setDate(today.getDate() - (WEEKS - 1) * DAYS - offset);

The second line was meant to be the correct version, overwriting the first.

The problem was that after the first call, start had already moved to a completely different month. The second call then used today.getDate() (for example, the 5th of March) but applied it to whatever month start had ended up in.

The resulting date landed somewhere in 2024, not 2025.

Every recent event mapped to an index far beyond the end of the array and was silently dropped.

The fix was simply to remove the first call and keep the correct calculation.


Bug Two: Tooltips Were Dead Silent

I added hover tooltips so you could see the date and event count for any cell. The logic worked perfectly in isolation, but hovering over the grid did absolutely nothing.

The reason was simple. The canvas had this CSS:

pointer-events: none;

I had originally added it so the canvas would not block clicks on the text underneath. Unfortunately, this also disables all mouse events, including mousemove. No events means no tooltip.

The fix was to attach the event listener to the parent container instead of the canvas. Since the canvas fills the parent exactly, the coordinate maths works the same.


The Fun Part: Special Days

Once the grid was working, I added colour coded special days.

Regular dates such as Christmas, my birthday, Easter, and Halloween appear in teal:

#3dbfb4

Space related dates appear in a dark pastel purple:

#a393c8

Examples include:

  • April 12 — Yuri’s Night
  • July 20 — Moon Landing Day
  • October 4 — Sputnik / World Space Week
  • November 2 — ISS Anniversary
  • January 28 / February 1 — Challenger and Columbia memorials

Easter is recalculated each year using the Anonymous Gregorian algorithm.

Hovering any cell shows a tooltip containing:

  • the full date
  • the event count
  • the special day name (if applicable)

The Logo

I had a new SVG logo and wanted it in the navigation next to the Hizhub wordmark, cycling through the same brand colours as the text.

The text uses a CSS gradient with background-clip: text and a rainbow-shift keyframe animation that moves the background position. That trick only works on text.

For the SVG I used a CSS mask instead:

.logo-icon {
    background: linear-gradient(135deg, var(--brand-blue), var(--brand-pink), ...);
    background-size: 300% 300%;
    animation: rainbow-shift 10s ease infinite;

    mask-image: url('/HizhubLogo.svg');
    mask-size: contain;
}

The gradient sits behind the SVG shape and is revealed through the mask. Because it uses the same animation and timing, it stays perfectly in sync with the text.


End Result

The CV header now includes a full width contribution grid that:

  • fills the entire section
  • responds to screen width
  • shows real commit history
  • highlights notable dates
  • displays hover tooltips

The contact form also got a redesign with floating labels, a custom subject dropdown, and a gradient border that matches the rest of the site styling.

It is the kind of detail most people will not notice. But the ones who do usually say something.