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 Source:https://dev.to/aairom/building-a-github-contribution-city-transforming-developer-history-into-a-3d-skyline-3haoRelated posts
Your AI Agent Is an Attacker's New API: Prompt Injection, Tool Hijacking, and Securing Local Coding Agents
Cursor vs Tabnine in 2026: One of These Tools Stopped Selling to New Customers
How to Hire AI Engineers in 2027: A Technical Hiring Guide

I Built an AI Dance Generator That Turns a Photo into a Video: Here's What I Learned"




