Back to Colophon
Back to Colophon

I wanted to see what I'm actually shipping, not just that I'm shipping. Built a GitHub activity dashboard with a local sync, static JSON, and two very different chart implementations.

#github#visualization#tooling#performance

Background#

GitHub’s contribution graph is a decent “am I shipping?” signal, but it hides the interesting part: what I’m shipping. I wanted a page that shows activity over time, breaks it down by repository, keeps private work visible without leaking repo names, and doesn’t make the homepage pay a big JavaScript tax.

The feature#

The /activity page is a stacked area chart of commits per day, split by repo, plus summary stats like streaks and most active repos. The homepage gets a small sparkline preview using the same underlying dataset.

Keeping it up to date#

This is intentionally static. I didn’t want deploys to depend on GitHub API availability or rate limits.

A local sync script pulls data and writes a JSON artifact that gets committed to the repo:

bash
npm run sync-activity

Under the hood it uses gh api graphql to fetch contribution data in year-long windows (GitHub caps contribution queries at one year). It stitches together repo-level commits, calendar totals, and private contribution counts, then merges overlapping windows into a single timeline. A visibility config controls what shows up in the chart: show, alias, hide, minimum threshold, plus a forceInclude escape hatch for repos that would otherwise fall below the cutoff.

If I want the dashboard current, I run the sync, commit the updated JSON, and push.

Private contributions (without repo leaks)#

Most of my work is in private repos, so a chart that only shows public commits would quietly undercount by a lot. GitHub’s contribution calendar includes private work in its daily totals but won’t attribute it to specific repositories. The sync calculates the gap between the calendar total and the sum of known repo commits each day, then assigns the difference to a Private series.

It’s not perfect, but it’s better than a chart that lies by omission.

UI implementation#

The /activity chart uses Recharts. It earns its weight there: axes, tooltips, interactive legend toggles, time range filtering.

The chart itself is straightforward. The shell around it is less so. Astro + View Transitions means DOM swaps on navigation, so the React chart is wrapped in a Web Component (<activity-chart>) and rendered with client:only="react". Recharts touches window internally, and the Web Component’s connectedCallback re-initializes after every transition.

Both chart components share a useChartTheme hook that reads CSS variables and watches for theme changes via MutationObserver. I considered just reading the values once on mount, but theme toggles would leave the chart stuck in the wrong palette until navigation.

Performance pass#

The homepage mini chart originally pulled in all of Recharts (shared chunk ~281KB) for a single <Area> with no axes, no tooltips, no legend. That’s a lot of JS for a sparkline.

The replacement is a direct SVG path with monotone cubic Hermite interpolation (Fritsch-Carlson, about 40 lines). Hover animation now mutates SVG path attributes via refs instead of cycling React state on every frame, which also cuts down GC pressure from per-frame allocations.

The dashboard chart keeps Recharts, but got two fixes:

  • Stable color indexing. Repo colors are pinned to their position in the full sorted list, so toggling visibility doesn’t reshuffle the palette.
  • Fade entrance instead of Recharts’ default left-to-right sweep, gated behind prefers-reduced-motion.

Trade-offs#

It’s not real-time. Activity updates when the sync runs and gets committed. GitHub’s data has hard edges too: one-year query windows, no private repo attribution. The Private series is inferred from the gap, not reported. Recharts stays on /activity where it earns its weight, but the homepage stays lightweight with the raw SVG.

I wanted to see what I'm actually shipping, not just that I'm shipping. Built a GitHub activity dashboard with a local sync, static JSON, and two very different chart implementations.