Build your contribution city!
Developer profiles are our digital resumes, but let's be honest: the standard 53-week GitHub contribution heatmap grid is starting to feel a bit flat. What if instead of looking at flat green squares, we could walk through a bustling cyberpunk metropolis, gaze up at towering skyscrapers built from our busiest commit weeks, and watch our coding history come alive in 3D?
In this post, we’ll explore GitHub Contribution City, a production-grade application built in Go that transforms any GitHub user's contribution history into a stunning 2D SVG skyline, a 3D isometric SVG, and an interactive WebGL 3D world powered by Three.js.
Before moving further, I want to clarify that the inspiration for this project comes from a blog post by Markus Eisele—a highly esteemed IBM colleague—which you can read here. Reading it gave me the idea to build this using IBM Bob. However, Markus is a true Java enthusiast who isn't particularly fond of languages like Python or JavaScript. Personally, I'm not a fan of Java :) so I asked Bob to adapt the codebase into Go instead. We all have our own brand of madness! 😵💫
Introduction & Project Inspiration
The initial spark for this project was inspired by Markus Eisele’s fantastic Quarkus Contribution City concept. While the original implementation was written in Java, we wanted to build a modern, lightning-fast, zero-dependency binary in Go that can run locally as a CLI tool, deploy as a high-performance web server, or run automatically via GitHub Actions.
Whether you want to generate a gorgeous SVG badge for your README.md, view your personal tech metropolis in 3D, or spin up an automated daily action that tracks your team's contributions, GitHub Contribution City turns your code commits into architectural marvels.
Detailed Architecture and Implementation
System Architecture
The application is structured around a clean modular pipeline: input configurations and remote API data feed into our core scene builder, which in turn delegates to specialized renderers.
Configuration Management (pkg/config)
The configuration manager handles environment variables, .env configuration files, and sensible defaults (such as defaulting to torvalds if no user is specified).
package config
import (
"bufio"
"os"
"strconv"
"strings"
)
// Config represents runtime configuration for GitHub Contribution City
type Config struct {
GitHubToken string `json:"github_token"`
GitHubUsername string `json:"github_username"`
Weeks int `json:"weeks"`
MaxHeight int `json:"max_height"`
Theme string `json:"theme"`
Port int `json:"port"`
Host string `json:"host"`
OutputDir string `json:"output_dir"`
InputDir string `json:"input_dir"`
}
// DefaultConfig returns default sensible parameters
func DefaultConfig() *Config {
return &Config{
GitHubToken: "",
GitHubUsername: "torvalds",
Weeks: 53,
MaxHeight: 14,
Theme: "github-dark",
Port: 8088,
Host: "0.0.0.0",
OutputDir: "output",
InputDir: "input",
}
}
// LoadConfig loads configuration from optional .env file and environment variables
func LoadConfig(envFile string) *Config {
cfg := DefaultConfig()
if envFile != "" {
_ = loadDotEnv(envFile)
} else if _, err := os.Stat(".env"); err == nil {
_ = loadDotEnv(".env")
}
if val := os.Getenv("GITHUB_TOKEN"); val != "" {
cfg.GitHubToken = strings.TrimSpace(val)
}
if val := os.Getenv("GITHUB_USERNAME"); val != "" {
cfg.GitHubUsername = strings.TrimSpace(val)
}
if val := os.Getenv("CITY_WEEKS"); val != "" {
if w, err := strconv.Atoi(val); err == nil && w >= 1 && w <= 53 {
cfg.Weeks = w
}
}
if val := os.Getenv("CITY_MAX_HEIGHT"); val != "" {
if h, err := strconv.Atoi(val); err == nil && h >= 2 && h <= 50 {
cfg.MaxHeight = h
}
}
if val := os.Getenv("CITY_THEME"); val != "" {
cfg.Theme = strings.TrimSpace(val)
}
if val := os.Getenv("PORT"); val != "" {
if p, err := strconv.Atoi(val); err == nil && p > 0 {
cfg.Port = p
}
}
return cfg
}
func loadDotEnv(filepath string) error {
file, err := os.Open(filepath)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
key := strings.TrimSpace(parts[0])
val := strings.TrimSpace(parts[1])
val = strings.Trim(val, `"'`)
if os.Getenv(key) == "" {
_ = os.Setenv(key, val)
}
}
}
return scanner.Err()
}
Fetching Contribution Calendars (pkg/github)
The GitHub client seamlessly switches between authenticated GraphQL API queries (supporting private and internal commits if a PAT is provided) and a robust HTML scraper fallback for public profiles.
package github
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// ContributionDay represents one day's contributions
type ContributionDay struct {
ContributionCount int `json:"contributionCount"`
ContributionLevel string `json:"contributionLevel"`
Date string `json:"date"`
Weekday int `json:"weekday"`
ParsedDate time.Time `json:"-"`
}
// ContributionWeek represents a week of days (up to 7 days)
type ContributionWeek struct {
ContributionDays []ContributionDay `json:"contributionDays"`
FirstDay string `json:"firstDay"`
}
// ContributionCalendar is the complete calendar returned by GitHub
type ContributionCalendar struct {
TotalContributions int `json:"totalContributions"`
Weeks []ContributionWeek `json:"weeks"`
Colors []string `json:"colors"`
Username string `json:"username"`
}
// Client is a GitHub API client that can fetch contribution calendars
type Client struct {
token string
httpClient *http.Client
graphQLURL string
}
func NewClient(token string) *Client {
return &Client{
token: token,
httpClient: &http.Client{
Timeout: 15 * time.Second,
},
graphQLURL: "https://api.github.com/graphql",
}
}
// FetchCalendar fetches contribution data using GraphQL or public scraping fallback
func (c *Client) FetchCalendar(ctx context.Context, username string) (*ContributionCalendar, error) {
if c.token != "" {
cal, err := c.fetchGraphQL(ctx, username)
if err == nil && cal != nil {
return cal, nil
}
}
return c.fetchPublicCalendar(ctx, username)
}
Logarithmic City Layout Algorithm (pkg/city)
To prevent single hyper-active weeks from completely distorting the skyline scale, building heights are calculated using logarithmic scaling:
// Calculate building heights using logarithmic scale as specified in Quarkus Contribution City:
buildingHeights := make([]int, numWeeks)
for i, tot := range weeklyTotals {
if tot == 0 || maxWeekly == 0 {
buildingHeights[i] = 1
} else {
ratio := math.Log1p(float64(tot)) / math.Log1p(float64(maxWeekly))
h := 1 + int(math.Round(ratio * float64(opts.MaxHeight-1)))
if h < 1 { h = 1 }
if h > opts.MaxHeight { h = opts.MaxHeight }
buildingHeights[i] = h
}
}
Main Application Entrypoint (cmd/city/main.go)
The main CLI dispatcher supports generating single SVGs, 3D isometric views, batch processing multiple usernames from an input folder, or starting an embedded HTTP server.
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
"github.com/alainairom/github-contribution-city/pkg/city"
"github.com/alainairom/github-contribution-city/pkg/config"
"github.com/alainairom/github-contribution-city/pkg/github"
"github.com/alainairom/github-contribution-city/pkg/renderer"
)
func main() {
var (
serverMode = flag.Bool("server", false, "Start the web visualization server")
username = flag.String("user", "", "GitHub username to visualize")
token = flag.String("token", "", "GitHub Personal Access Token (PAT)")
weeks = flag.Int("weeks", 0, "Number of weeks to render (4 - 53)")
maxHeight = flag.Int("height", 0, "Maximum building height (3 - 30)")
theme = flag.String("theme", "", "Visual theme")
format = flag.String("format", "svg", "Export format: svg, iso, html, json, all")
outFile = flag.String("output", "", "Output filepath or directory")
port = flag.Int("port", 0, "HTTP server port")
envFile = flag.String("env", ".env", "Path to .env configuration file")
)
flag.Parse()
cfg := config.LoadConfig(*envFile)
if *username != "" { cfg.GitHubUsername = *username }
if *token != "" { cfg.GitHubToken = *token }
if *weeks > 0 { cfg.Weeks = *weeks }
if *maxHeight > 0 { cfg.MaxHeight = *maxHeight }
if *theme != "" { cfg.Theme = *theme }
if *port > 0 { cfg.Port = *port }
_ = os.MkdirAll(cfg.OutputDir, 0755)
client := github.NewClient(cfg.GitHubToken)
builder := city.NewBuilder()
svgRenderer := renderer.NewSVGRenderer()
htmlRenderer := renderer.NewHTMLRenderer()
if *serverMode {
runServer(cfg, client, builder, svgRenderer, htmlRenderer)
return
}
fmt.Printf("🏙️ Generating Contribution City for '%s'...\n", cfg.GitHubUsername)
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
cal, err := client.FetchCalendar(ctx, cfg.GitHubUsername)
if err != nil {
log.Fatalf("❌ Error fetching contribution data: %v", err)
}
scene := builder.Build(cal, city.Options{
Weeks: cfg.Weeks,
MaxHeight: cfg.MaxHeight,
Theme: cfg.Theme,
})
outPath := filepath.Join(cfg.OutputDir, fmt.Sprintf("%s_city.svg", cfg.GitHubUsername))
svgContent := svgRenderer.RenderSkyline(scene, cfg.Theme)
_ = os.WriteFile(outPath, []byte(svgContent), 0644)
fmt.Printf("✨ SVG Skyline successfully written to: %s\n", outPath)
}
Automation with GitHub Actions
To keep your contribution city continuously updated without manual intervention, the repository features an automated CI/CD and scheduling suite using GitHub Actions.
The workflow automation includes:
-
Continuous Integration (ci.yml): Runs linting checks,
go vet, vulnerability scans viagovulncheck, multi-OS unit tests (ubuntu,macos,windows), and cross-compilation on every push and pull request.
name: Continuous Integration
on:
push:
branches:
- main
- master
paths-ignore:
- 'Docs/**'
- 'README.md'
- 'output/**'
pull_request:
branches:
- main
- master
permissions:
contents: read
jobs:
lint:
name: Lint & Code Style
runs-on: ubuntu-latest
steps:
- name: Checkout Source Code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
cache: true
- name: Verify Formatting
run: |
UNFORMATTED=$(gofmt -l . | grep -v '^vendor/' || true)
if [ -n "$UNFORMATTED" ]; then
echo "❌ Code format issues found in following files:"
echo "$UNFORMATTED"
exit 1
fi
echo "✅ All Go files formatted properly."
- name: Run Go Vet
run: go vet ./...
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: latest
args: --timeout=5m
continue-on-error: true
vuln-scan:
name: Security & Vulnerability Scan
runs-on: ubuntu-latest
steps:
- name: Checkout Source Code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
cache: true
- name: Run govulncheck
uses: golang/govulncheck-action@v1
with:
go-version-input: '1.24'
go-package: ./...
...
-
Daily Visualization Updates (update-city.yml): Powered by a scheduled cron trigger (
03:17 UTC), this workflow fetches fresh contribution matrices for the repository owner and any tracked usernames listed inside theinput/folder, regenerates the respective skylines, and commits the updated graphics back to the repository automatically ([skip ci]).
name: Daily Contribution City Update
on:
schedule:
# Runs everyday at 03:17 UTC
- cron: '17 3 * * *'
workflow_dispatch:
inputs:
custom_user:
description: 'Optional specific GitHub username to generate for'
required: false
default: ''
theme:
description: 'Theme to use (github-dark, github-light, cyberpunk, mono, neon, matrix)'
required: false
default: 'github-dark'
permissions:
contents: write
jobs:
update-visualizations:
name: Regenerate Contribution Cities
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
cache: true
- name: Build Contribution City CLI
run: |
go build -o bin/github-contribution-city cmd/city/main.go
- name: Generate Visualizations
env:
GITHUB_TOKEN: ${{ secrets.CONTRIBUTION_TOKEN || secrets.GITHUB_TOKEN }}
run: |
mkdir -p output
PRIMARY_USER="${{ inputs.custom_user || github.repository_owner }}"
SELECTED_THEME="${{ inputs.theme || 'github-dark' }}"
echo "🌆 Generating contribution city for primary user: ${PRIMARY_USER} (Theme: ${SELECTED_THEME})..."
# 1. Generate primary user 2D SVG Skyline
./bin/github-contribution-city \
-user "${PRIMARY_USER}" \
-weeks 53 \
-height 14 \
-theme "${SELECTED_THEME}" \
-format svg \
-output "output/contribution-city.svg"
# 2. Generate primary user 3D Isometric SVG
./bin/github-contribution-city \
-user "${PRIMARY_USER}" \
-weeks 53 \
-height 14 \
-theme "cyberpunk" \
-format iso \
-output "output/contribution-city-3d.svg"
# 3. Generate primary user 3D Interactive HTML
./bin/github-contribution-city \
-user "${PRIMARY_USER}" \
-weeks 53 \
-height 14 \
-theme "${SELECTED_THEME}" \
-format html \
-output "output/contribution-city.html"
# 4. If tracked users exist in input/ directory, batch process them
if [ -d "input" ] && [ "$(ls -A input 2>/dev/null)" ]; then
echo "📁 Processing batch users from input directory..."
./bin/github-contribution-city -batch input/ -output output/
fi
- name: Commit and Push Changes
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git add output/
if git diff --staged --quiet; then
echo "ℹ️ No changes detected in contribution calendars. Skipping commit."
else
git commit -m "chore(city): auto-update contribution city visualizations [skip ci]"
git push
echo "✅ Successfully committed and pushed updated visualizations."
fi
- name: Upload Output Artifacts
uses: actions/upload-artifact@v4
with:
name: contribution-city-visualizations
path: output/
retention-days: 30
-
Container & Release Management (docker.yml & release.yml): Automatically builds multi-arch container images (
linux/amd64,linux/arm64) for the GitHub Container Registry (ghcr.io) and packages cross-compiled release binaries whenever version tags are pushed.
Conclusion
Building GitHub Contribution City was a fun journey merging developer metrics with procedural city generation and graphics rendering. By leveraging Go, we achieved a blazing fast binary that can be embedded directly into CI/CD pipelines, containerized for Docker/Podman, or served live via an interactive Three.js 3D browser experience.
Thanks for reading 🌃
Links
Github repository for this post: https://github.com/aairom/github-contribution-city
Build a GitHub Contribution City With Java and Quarkus (by Markus Eisele): https://dzone.com/articles/build-a-github-contribution-city-with-java-and-qua
Quarkus-Contribution-City (java code from Markus-Eisele): https://github.com/myfear/quarkus-contribution-city





Top comments (0)