My GitHub contribution graph is a metro train now.
A few days ago I listed my GitHub profile on the stock market. The stock, $VJK, gets its value from my commits, but the commits themselves were still only a number in a panel. I wanted the contribution graph on the profile too, drawn as something other than green squares.
Bengaluru's Namma Metro runs on long elevated viaducts across the city, so the graph became a metro line.
How the graph becomes a train
GitHub's graph is 53 columns by 7 rows: a column for every week, a square for every day. Turn it on its side and it's a train.
- Every coach is a week. There are 53 of them.
- Every coach has seven windows, Sunday to Saturday.
- A window lights up with that day's commits. No commits, it stays dark. A busy day, it glows bright gold.
- The oldest week rides at the tail. This week's coach is right behind the driver's cab, and today's window pulses.
My busiest repos hang on sign gantries over the line, each one above the coach of the week I committed to it the most. The line ends at Commit Street, the interchange where the green line carries $VJK, with a small LED board showing the live price.
The ride
The panel loops every 90 seconds:
- It rests on today, with the driver's cab at the Commit Street platform.
- It fades, goes back a year, and picks up the tail of the train.
- The train runs along the viaduct while the camera moves along it a little faster, from the oldest coach toward the newest.
- Under each repo gantry the train slows down and the camera zooms in. Between gantries it speeds up and the camera zooms out.
- It brakes into Commit Street, and the loop starts again.
Passing under a gantry:
Arriving at Commit Street:
Zoomed out at full speed, months of coaches go by:
It follows your GitHub theme, so light mode gets a daytime version:
It took a few tries
The first version gave every week its own 7-coach train, with a coach for each day. The camera panned along a row of them, and they didn't move. It looked like a camera driving past parked trains.
So I fixed the camera and let the trains run through. That looked better, but now the stations had nowhere to go, because a station can't move along with a train.
The next version moved both, and that idea stayed: the camera moves faster than the trains, so they run forward while it overtakes them. Then I put gaps between the trains, because 53 trains nose to tail looked like one endless train.
And then the obvious question: why are 53 trains sharing one track? One long train fixed that, and it maps more directly onto the graph too, one coach per week.
The last change was the stations. A 53-coach train stopping with a coach from March at the platform looks wrong, because a real train stops with the driver at the front of the platform. So repos became sign gantries over the line, the train only slows down under them, and the one real stop is Commit Street.
How it works
It's still just an SVG
GitHub doesn't run JavaScript in a README. Like everything else on my profile, Commit Metro is an SVG loaded through an <img> tag and animated with CSS. A Node script redraws it from my contribution data every time the profile's GitHub Actions workflow runs, so new commits show up on their own.
Keeping each gantry above the right coach
The train and the camera move at different speeds, so how does a gantry end up exactly above the right coach? It's built there. If week u is the coach in focus, the camera looks at world(u), and the train has moved world(u) - centre(u):
const centre = (u) => coachX(u) + COACH / 2; // coach u, in the train's own coordinates
const world = (u) => STRETCH * centre(u) + W; // where the camera looks
const runAt = (u) => world(u) - centre(u); // how far the train has moved along the line
With STRETCH = 3 the camera covers three times the distance the train does. The gantry for week i stands at world(i), so whenever the camera reaches it, coach i is right underneath.
Speeding up and slowing down without a jolt
Each stretch between gantries is two CSS cubic-bezier curves, one that speeds up and one that slows down:
const EASE = {
speedUp: (from) => `cubic-bezier(.3,${0.3 * from},.7,${1 - 0.3 * PEAK})`,
slowDown: (to) => `cubic-bezier(.3,${0.3 * PEAK},.7,${1 - 0.3 * to})`,
};
The slope at each end of a cubic-bezier is the speed at that end. So the two halves meet at the same top speed in the middle, and a stretch arrives at a gantry at the same slow speed the next one leaves with. Every stretch takes time in proportion to its length, so they all share one average speed. I sampled the motion to check it: under a gantry the train runs at 0.44 of its average speed, and it peaks at 1.62.
The zoom is one more keyframe animation with the same percentages and the same curves, zoomed in at the gantries and out where the train is fastest. Because every animated layer shares that timing, the camera, the train and the zoom stay in step.
A first frame that makes sense
Some places that show a README never play the animation, like screenshots and some apps, and people with reduced motion turned on don't get it either. So the resting frame, with the train at Commit Street, is also the starting state. Anything that doesn't animate still shows today.
The driver's cab
The nose is one cubic Bézier curve from the roof down to the tip, extended back into depth for the top of the nose. The rear cab uses the same curve, flipped. The front one also has a small horn on the roof and two headlights whose beams fade out down the track.
Where the gantries come from
GitHub's GraphQL API returns commits per repo per day:
contributionsCollection {
commitContributionsByRepository(maxRepositories: 25) {
repository { name isPrivate }
contributions(first: 100) { nodes { occurredAt commitCount } }
}
}
I group each repo's commits by week and keep my six busiest public repos. Private repos are left out, so their names never show up. If two repos have the same busiest week, they share one gantry and its board lists both.
Try it
Commit Metro is at the top of my profile. The code lives in the same repo, mostly in scripts/lib/render/metro.js, if you want a train of your own.
If your graph has a lot of empty windows, like mine does in August, that's what it looks like on a train too.







Top comments (1)
Ý tưởng biến biểu đồ đóng góp thành sơ đồ tàu điện ngầm thực sự rất sáng tạo và có tính cá nhân hóa cao. Việc trực quan hóa dữ liệu thô từ GitHub thành một hình ảnh có ý nghĩa nghệ thuật giúp việc theo dõi tiến độ code trở nên thú vị hơn hẳn thay vì chỉ nhìn vào những ô vuông xanh đơn điệu. Mình từng thử làm một vài dashboard tương tự bằng D3.js nhưng cái khó nhất luôn là việc xử lý dữ liệu lịch sử sao cho mượt mà mà không làm treo trình duyệt khi số lượng commit quá lớn. Nếu bạn định mở rộng dự án này để hỗ trợ các loại dữ liệu khác như commit message hay thời gian làm việc trong ngày thì chắc chắn kết quả sẽ còn ấn tượng hơn nữa.