🏙️ The Idea
Everyone's seen the GitHub contribution graph — that grid of green squares that quietly judges your consistency. It's useful, but it's also boring. I wanted to take the same underlying data (commits per day) and render it as a procedural skyline: a tiny city where each building's height and color are derived from your actual git history.
This post walks through the three layers of that project:
- Parsing
git loginto a clean time-series data structure - Mapping commit density to visual properties (height, color, spacing)
- Rendering the result as SVG — and the trade-offs I made between accuracy and looking cool
🔍 Step 1: Getting Real Data Out of Git
The naive approach is git log --since=... --pretty=format:%ad, but that only gets you commit dates, not density grouped cleanly by day. I used --date=short to normalize timestamps and avoid timezone chaos:
bash
git log --all --date=short --pretty=format:'%ad' | sort | uniq -c
That gives output like:
3 2024-01-02
7 2024-01-03
1 2024-01-05
I parse this in Python into a dictionary keyed by date, defaulting missing days to zero so the skyline doesn't silently skip empty lots:
python
import subprocess
from collections import defaultdict
from datetime import date, timedelta
def get_commit_counts(since_days=180):
raw = subprocess.run(
["git", "log", "--all", "--date=short", "--pretty=format:%ad"],
capture_output=True, text=True
).stdout
counts = defaultdict(int)
for line in raw.splitlines():
counts[line.strip()] += 1
today = date.today()
series = {}
for i in range(since_days):
d = today - timedelta(days=i)
key = d.isoformat()
series[key] = counts.get(key, 0)
return dict(sorted(series.items()))
Nothing fancy, but this is the foundation everything else builds on — literally. Garbage in, garbage skyline out.
🏗️ Step 2: From Commits to Buildings
Here's where the real design decisions start. A raw linear mapping (1 commit = 1 pixel of height) looks terrible in practice. Most days have 0–5 commits, with rare spikes of 40+ on refactor days. Graphed linearly, you get a flat city with one skyscraper piercing the clouds and nothing else visible.
I tried three height functions before settling on one:
python
import math
def linear_height(count, max_h=120):
return count * 4 # ugly, spiky, unreadable
def sqrt_height(count, max_h=120):
return min(max_h, math.sqrt(count) * 18)
def log_height(count, max_h=120):
return min(max_h, math.log2(count + 1) * 22)
sqrt_height won. Log compression flattened too much — a 2-commit day and a 20-commit day looked nearly identical, which felt dishonest. Square root kept meaningful separation between "quiet day," "normal day," and "went feral refactoring day" while still capping outliers so one hackathon doesn't dwarf six months of steady work.
This is the core trade-off of the whole project: a perfectly accurate graph is visually dead, and a perfectly pretty graph lies about your habits. Square root height is the compromise — it preserves rank order and rough relative magnitude, but sacrifices exact proportionality for something your eye can actually parse.
🎨 Step 3: Color Buckets Instead of Gradients
My first version used a smooth HSL gradient keyed to commit count. It looked like a thermal camera, not a city. Continuous gradients read as noise when you've got 180 buildings in a row — your brain can't bucket them into categories, so there's no story.
Switching to discrete color buckets (same idea as GitHub's own 5-shade system) fixed it immediately:
python
DAY_COLORS = [
(0, "#1b1f23"), # empty lot
(1, "#2d4a3e"), # lights on
(4, "#3f7d5c"), # active
(10, "#5fb98a"), # busy
(20, "#9be8bb"), # blazing
]
def bucket_color(count):
color = DAY_COLORS[0][1]
for threshold, hex_color in DAY_COLORS:
if count >= threshold:
color = hex_color
return color
Buckets impose categories on continuous data, which is technically a loss of information — but that loss is exactly what makes it scannable at a glance. This is the same reasoning behind histogram binning or log-scale axes: sometimes resolution is the enemy of comprehension.
🖼️ Step 4: Rendering the Skyline as SVG
SVG was the obvious output format: scriptable, scalable, and embeddable directly in a README. Each day becomes a <rect>, positioned along the x-axis by date and sized by the height function:
python
def render_svg(series, width=720, height=160, building_width=4, gap=1):
bars = []
x = 0
for date_str, count in series.items():
h = sqrt_height(count, max_h=height - 10)
color = bucket_color(count)
y = height - h
bars.append(
f'
)
x += building_width + gap
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{x}" height="{height}">
<rect width="100%" height="100%" fill="#0d1117" />
{"".join(bars)}
</svg>'''
return svg
A few cosmetic touches made a surprisingly big difference:
- Jittering building width ±1px based on a hash of the date, so the skyline doesn't look like a bar chart wearing a trench coat
- Random "window" rects overlaid on taller buildings (height > 60) purely for texture — zero data value, 100% vibes
- A flat baseline shadow under every bar so zero-commit days still read as "empty lot," not "rendering bug"
None of these affect the data. All of them affect whether someone stops scrolling.
⚖️ Accuracy vs. Appeal: Where I Drew the Line
To be explicit about the trade-offs, since that's the interesting part of this project, not the SVG syntax:
| Decision | Accurate choice | Chosen choice | Why |
|---|---|---|---|
| Height scaling | Linear | Square root | Linear hides low-activity days entirely |
| Color mapping | Continuous gradient | 5 discrete buckets | Gradients aren't scannable at a glance |
| Max height cap | None | Capped at 120px | One huge day shouldn't dwarf six months |
| Zero-commit days | Omit | Render as dark empty lot | Gaps are data too |
| Building texture (windows) | N/A | Randomized, non-data-driven | Pure aesthetics, clearly separated from data logic |
The common thread: every distortion is a readability decision, not a dishonesty decision. Rank order and relative scale are preserved everywhere. Nothing here would mislead someone into thinking a quiet week was a busy one — it just makes the busy weeks easier to spot among 180 data points instead of lost in a wall of near-identical greens.
🚀 What's Next
Current roadmap: layering multiple repos into one skyline (a "district" per project), animating growth over time as a GIF for README headers, and maybe building height curves per author instead of per day for team repos.
Have you built visualizations on top of git log before, or found a height/color mapping that worked better than square-root scaling? I'd genuinely like to compare notes — drop your approach in the comments.
Full source (parsing + rendering + a CLI wrapper) is on GitHub if you want to point it at your own repo and see what kind of skyline your commit history builds.
Top comments (1)
Do the tiny window details still show up when GitHub scales the SVG down on mobile?