Published on

Visualizing Your GitHub Contributions: A Complete Guide

Authors

Visualizing Your GitHub Contributions

As developers, our GitHub profiles are an important part of our professional identity. They showcase our work, contributions, and activity in the open-source community. However, if you're like me and have multiple GitHub accounts (perhaps one for personal projects, one for work, and another for a specific organization), it can be challenging to get a comprehensive view of your total contributions.

In this guide, I'll walk you through how I built the GitHub stats page for this website, which combines data from my three GitHub accounts and presents a unified dashboard of my contribution activity.

The Goal: Unified GitHub Stats

My primary account is @je-ramos, but I also contribute through two other accounts. I wanted to create a dashboard that would show:

  • Total contributions across all accounts (currently 608 contributions in the last year)
  • A visual contribution calendar (similar to GitHub's)
  • Breakdown of activity by type (commits, PRs, issues, reviews)
  • Top language statistics
  • Featured repositories

Setting Up the GitHub GraphQL API

The GitHub REST API is great for simple queries, but for more advanced data collection, the GraphQL API is much more powerful and efficient. Here's how to set it up:

1. Generate a Personal Access Token

First, you need to create a Personal Access Token with the appropriate permissions:

  1. Go to GitHub Settings > Developer settings > Personal access tokens
  2. Click "Generate new token (classic)"
  3. Give it a descriptive name like "GitHub Stats Fetcher"
  4. Select the following scopes:
    • repo (for private repository stats if needed)
    • read:user (for your user data)
    • user:email (for email information)
  5. Generate the token and save it somewhere secure

2. Create a Script to Fetch the Data

I created a Node.js script that uses the GitHub GraphQL API to fetch contribution data from all my accounts. Here's the core of the script:

// fetch-github-stats.js
const fetch = require('node-fetch')
const fs = require('fs')
const path = require('path')

// Configuration - update with your accounts
const CONFIG = {
  TOKEN: process.env.GITHUB_TOKEN || 'your-token-here',
  USERNAMES: ['je-ramos', 'your-other-account', 'your-third-account'],
  OUTPUT_PATH: path.join(__dirname, '../public/data/github-stats.json'),
}

// GraphQL query for user statistics
const getUserStatsQuery = `
query UserStats($username: String!) {
  user(login: $username) {
    name
    login
    contributionsCollection {
      contributionCalendar {
        totalContributions
        weeks {
          contributionDays {
            date
            contributionCount
            color
          }
        }
      }
      commitContributionsByRepository {
        contributions {
          totalCount
        }
      }
      issueContributionsByRepository {
        contributions {
          totalCount
        }
      }
      pullRequestContributionsByRepository {
        contributions {
          totalCount
        }
      }
      pullRequestReviewContributionsByRepository {
        contributions {
          totalCount
        }
      }
    }
    repositories(first: 100, orderBy: {field: STARGAZERS, direction: DESC}) {
      totalCount
      nodes {
        name
        description
        stargazerCount
        url
        languages(first: 10, orderBy: {field: SIZE, direction: DESC}) {
          edges {
            size
            node {
              name
              color
            }
          }
        }
        isPrivate
      }
    }
  }
}
`

async function fetchGitHubStats() {
  try {
    let combinedStats = {
      totalContributions: 0,
      contributionsByAccount: [],
      contributionsByType: {
        commits: 0,
        issues: 0,
        pullRequests: 0,
        reviews: 0,
      },
      contributionCalendar: [],
      topLanguages: {},
      pinnedRepos: [],
      lastUpdated: new Date().toISOString(),
    }

    // Process each username
    for (const username of CONFIG.USERNAMES) {
      const response = await fetch('https://api.github.com/graphql', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${CONFIG.TOKEN}`,
        },
        body: JSON.stringify({
          query: getUserStatsQuery,
          variables: { username },
        }),
      })

      const data = await response.json()
      if (data.errors) {
        console.error(`Error fetching data for ${username}:`, data.errors)
        continue
      }

      if (!data.data?.user) {
        console.warn(`User ${username} not found`)
        continue
      }

      const { user } = data.data
      const contributions = user.contributionsCollection

      // Add to total contributions
      const totalUserContributions = contributions.contributionCalendar.totalContributions
      combinedStats.totalContributions += totalUserContributions

      // Account-specific contributions
      combinedStats.contributionsByAccount.push({
        name: user.name || username,
        login: username,
        count: totalUserContributions,
      })

      // Contribution types
      let commitCount = 0
      let issueCount = 0
      let prCount = 0
      let reviewCount = 0

      contributions.commitContributionsByRepository.forEach((repo) => {
        commitCount += repo.contributions.totalCount
      })

      contributions.issueContributionsByRepository.forEach((repo) => {
        issueCount += repo.contributions.totalCount
      })

      contributions.pullRequestContributionsByRepository.forEach((repo) => {
        prCount += repo.contributions.totalCount
      })

      contributions.pullRequestReviewContributionsByRepository.forEach((repo) => {
        reviewCount += repo.contributions.totalCount
      })

      combinedStats.contributionsByType.commits += commitCount
      combinedStats.contributionsByType.issues += issueCount
      combinedStats.contributionsByType.pullRequests += prCount
      combinedStats.contributionsByType.reviews += reviewCount

      // Process calendar data (simplified for brevity)
      // ...

      // Process repositories for languages
      const publicRepos = user.repositories.nodes.filter((repo) => !repo.isPrivate)

      // Add language data
      publicRepos.forEach((repo) => {
        if (repo.languages && repo.languages.edges) {
          repo.languages.edges.forEach((edge) => {
            const { name, color } = edge.node
            if (!combinedStats.topLanguages[name]) {
              combinedStats.topLanguages[name] = {
                name,
                color,
                size: 0,
              }
            }
            combinedStats.topLanguages[name].size += edge.size
          })
        }
      })

      // Add top repositories
      const topRepos = publicRepos
        .sort((a, b) => b.stargazerCount - a.stargazerCount)
        .slice(0, 4)
        .map((repo) => ({
          name: repo.name,
          description: repo.description,
          stars: repo.stargazerCount,
          url: repo.url,
        }))

      combinedStats.pinnedRepos.push(...topRepos)
    }

    // Calculate percentages for contribution types
    const totalActivities =
      combinedStats.contributionsByType.commits +
      combinedStats.contributionsByType.issues +
      combinedStats.contributionsByType.pullRequests +
      combinedStats.contributionsByType.reviews

    combinedStats.contributionsByType.commitsPercentage = Math.round(
      (combinedStats.contributionsByType.commits / totalActivities) * 100
    )
    combinedStats.contributionsByType.issuesPercentage = Math.round(
      (combinedStats.contributionsByType.issues / totalActivities) * 100
    )
    combinedStats.contributionsByType.pullRequestsPercentage = Math.round(
      (combinedStats.contributionsByType.pullRequests / totalActivities) * 100
    )
    combinedStats.contributionsByType.reviewsPercentage = Math.round(
      (combinedStats.contributionsByType.reviews / totalActivities) * 100
    )

    // Process languages into percentages
    const totalSize = Object.values(combinedStats.topLanguages).reduce(
      (sum, lang) => sum + lang.size,
      0
    )
    combinedStats.topLanguages = Object.values(combinedStats.topLanguages)
      .map((lang) => ({
        name: lang.name,
        color: lang.color,
        percentage: Math.round((lang.size / totalSize) * 100),
      }))
      .sort((a, b) => b.percentage - a.percentage)
      .slice(0, 5)

    // Sort pinned repos by stars
    combinedStats.pinnedRepos = combinedStats.pinnedRepos
      .sort((a, b) => b.stars - a.stars)
      .slice(0, 6)

    // Write output to file
    fs.mkdirSync(path.dirname(CONFIG.OUTPUT_PATH), { recursive: true })
    fs.writeFileSync(CONFIG.OUTPUT_PATH, JSON.stringify(combinedStats, null, 2))

    console.log(`GitHub stats successfully written to ${CONFIG.OUTPUT_PATH}`)
  } catch (error) {
    console.error('Error fetching GitHub stats:', error)
    process.exit(1)
  }
}

fetchGitHubStats()

3. Running the Script

To run the script, you'll need to set your GitHub token as an environment variable:

# Install dependencies
npm install node-fetch

# Run the script with token
GITHUB_TOKEN=your_token_here node scripts/fetch-github-stats.js

For automation, you can add this as a script in your package.json:

"scripts": {
  "fetch-github-stats": "node scripts/fetch-github-stats.js"
}

And then run it with:

GITHUB_TOKEN=your_token_here npm run fetch-github-stats

Creating a Visualization Dashboard

Now that we have our stats in JSON format, we can create a visualization dashboard. I created a dedicated page at /github in my Next.js app that reads this JSON file and displays the data in a visually appealing way.

Here's a simplified version of my GitHub Stats page component:

// app/github/page.tsx
import fs from 'fs'
import path from 'path'

export default function GitHubStatsPage() {
  // Read the JSON file
  const jsonPath = path.join(process.cwd(), 'public/data/github-stats.json')
  const githubStats = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))

  return (
    <div className="divide-y divide-gray-200 dark:divide-gray-700">
      <div className="space-y-2 pt-6 pb-8 md:space-y-5">
        <h1 className="text-3xl leading-9 font-extrabold tracking-tight text-gray-900 sm:text-4xl sm:leading-10 md:text-6xl md:leading-14 dark:text-gray-100">
          GitHub Stats
        </h1>
        <p className="text-lg leading-7 text-gray-500 dark:text-gray-400">
          A consolidated view of my open source contributions across{' '}
          {githubStats.contributionsByAccount.length} GitHub accounts.
        </p>
      </div>

      {/* Contributions Counter */}
      <div className="py-10">
        <div className="contribution-header">
          <h2 className="text-2xl font-bold">
            {githubStats.totalContributions} contributions in the last year
          </h2>
        </div>

        {/* Contribution Calendar would go here */}
        <div className="contribution-calendar mt-4">{/* Calendar visualization component */}</div>
      </div>

      {/* Activity Breakdown */}
      <div className="py-10">
        <h2 className="mb-6 text-2xl font-bold">Activity Overview</h2>
        <div className="activity-breakdown grid grid-cols-1 gap-8 md:grid-cols-2">
          <div className="activity-stats">
            <div className="stat-item mb-4">
              <div className="flex items-center justify-between">
                <span>Commits</span>
                <span>{githubStats.contributionsByType.commitsPercentage}%</span>
              </div>
              <div className="mt-2 h-2 rounded-full bg-gray-200">
                <div
                  className="h-2 rounded-full bg-green-500"
                  style={{ width: `${githubStats.contributionsByType.commitsPercentage}%` }}
                ></div>
              </div>
            </div>

            <div className="stat-item mb-4">
              <div className="flex items-center justify-between">
                <span>Pull Requests</span>
                <span>{githubStats.contributionsByType.pullRequestsPercentage}%</span>
              </div>
              <div className="mt-2 h-2 rounded-full bg-gray-200">
                <div
                  className="h-2 rounded-full bg-blue-500"
                  style={{ width: `${githubStats.contributionsByType.pullRequestsPercentage}%` }}
                ></div>
              </div>
            </div>

            <div className="stat-item mb-4">
              <div className="flex items-center justify-between">
                <span>Issues</span>
                <span>{githubStats.contributionsByType.issuesPercentage}%</span>
              </div>
              <div className="mt-2 h-2 rounded-full bg-gray-200">
                <div
                  className="h-2 rounded-full bg-yellow-500"
                  style={{ width: `${githubStats.contributionsByType.issuesPercentage}%` }}
                ></div>
              </div>
            </div>

            <div className="stat-item">
              <div className="flex items-center justify-between">
                <span>Code Reviews</span>
                <span>{githubStats.contributionsByType.reviewsPercentage}%</span>
              </div>
              <div className="mt-2 h-2 rounded-full bg-gray-200">
                <div
                  className="h-2 rounded-full bg-purple-500"
                  style={{ width: `${githubStats.contributionsByType.reviewsPercentage}%` }}
                ></div>
              </div>
            </div>
          </div>

          <div className="activity-chart">{/* Radar or other chart visualization */}</div>
        </div>
      </div>

      {/* Top Languages */}
      <div className="py-10">
        <h2 className="mb-6 text-2xl font-bold">Top Languages</h2>
        <div className="space-y-4">
          {githubStats.topLanguages.map((lang, i) => (
            <div key={i} className="relative">
              <div className="mb-1 flex justify-between">
                <span className="text-base font-medium">
                  <span
                    className="mr-2 inline-block h-3 w-3 rounded-full"
                    style={{ backgroundColor: lang.color || '#ccc' }}
                  ></span>
                  {lang.name}
                </span>
                <span className="text-sm font-medium">{lang.percentage}%</span>
              </div>
              <div className="h-2.5 w-full rounded-full bg-gray-200 dark:bg-gray-700">
                <div
                  className="h-2.5 rounded-full"
                  style={{
                    width: `${lang.percentage}%`,
                    backgroundColor: lang.color || '#ccc',
                  }}
                ></div>
              </div>
            </div>
          ))}
        </div>
      </div>

      {/* Pinned Repositories */}
      <div className="py-10">
        <h2 className="mb-6 text-2xl font-bold">Featured Repositories</h2>
        <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
          {githubStats.pinnedRepos.map((repo, i) => (
            <a
              key={i}
              href={repo.url}
              target="_blank"
              rel="noopener noreferrer"
              className="hover:border-primary-500 rounded-lg border border-gray-200 bg-white p-6 transition dark:border-gray-700 dark:bg-gray-800"
            >
              <div className="flex items-start justify-between">
                <h3 className="text-primary-600 text-lg font-medium">{repo.name}</h3>
                <div className="flex items-center">
                  <svg
                    className="h-4 w-4 text-yellow-400"
                    fill="currentColor"
                    viewBox="0 0 20 20"
                    xmlns="http://www.w3.org/2000/svg"
                  >
                    <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path>
                  </svg>
                  <span className="ml-1 text-sm text-gray-600">{repo.stars}</span>
                </div>
              </div>
              <p className="mt-2 text-sm text-gray-600">{repo.description}</p>
            </a>
          ))}
        </div>
      </div>
    </div>
  )
}

Building the Contribution Calendar

One of the most challenging parts was recreating GitHub's contribution calendar. For this, I created a custom component that visualizes the data similar to GitHub's calendar:

// components/ContributionCalendar.jsx
import { useMemo } from 'react'

export default function ContributionCalendar({ data }) {
  const months = [
    'Jan',
    'Feb',
    'Mar',
    'Apr',
    'May',
    'Jun',
    'Jul',
    'Aug',
    'Sep',
    'Oct',
    'Nov',
    'Dec',
  ]
  const days = ['Mon', 'Wed', 'Fri']

  // Group contribution data by week and day
  const calendarData = useMemo(() => {
    // Process the raw data into a format suitable for the calendar
    // This would transform the GitHub API response into a grid format
    return data
  }, [data])

  return (
    <div className="contribution-calendar text-sm">
      <div className="mb-1 flex">
        <div className="w-8"></div>
        {months.map((month, i) => (
          <div key={i} className="flex-1 text-center">
            {month}
          </div>
        ))}
      </div>

      <div className="flex">
        <div className="w-8 pr-2">
          {days.map((day, i) => (
            <div key={i} className="h-4 text-right" style={{ marginTop: i === 0 ? '0' : '8px' }}>
              {day}
            </div>
          ))}
        </div>

        <div className="grid flex-1 grid-cols-52 gap-1">
          {calendarData.map((week, weekIndex) => (
            <div key={weekIndex} className="flex flex-col gap-1">
              {week.map((day, dayIndex) => (
                <div
                  key={`${weekIndex}-${dayIndex}`}
                  className="h-3 w-3 rounded-sm"
                  style={{ backgroundColor: day.color }}
                  title={`${day.count} contributions on ${day.date}`}
                ></div>
              ))}
            </div>
          ))}
        </div>
      </div>

      <div className="mt-2 flex items-center justify-end text-xs text-gray-600">
        <span>Less</span>
        <div className="mx-2 flex gap-1">
          <div className="h-3 w-3 rounded-sm bg-gray-200"></div>
          <div className="h-3 w-3 rounded-sm bg-green-200"></div>
          <div className="h-3 w-3 rounded-sm bg-green-400"></div>
          <div className="h-3 w-3 rounded-sm bg-green-600"></div>
          <div className="h-3 w-3 rounded-sm bg-green-800"></div>
        </div>
        <span>More</span>
      </div>
    </div>
  )
}

Setting Up Automated Updates

To keep the stats up to date, I set up a GitHub Action that runs the script daily:

# .github/workflows/update-github-stats.yml
name: Update GitHub Stats

on:
  schedule:
    - cron: '0 0 * * *' # Run daily at midnight
  workflow_dispatch: # Allow manual trigger

jobs:
  update-stats:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install dependencies
        run: npm ci

      - name: Fetch GitHub stats
        run: node scripts/fetch-github-stats.js
        env:
          GITHUB_TOKEN: ${{ secrets.GH_STATS_TOKEN }}

      - name: Commit and push if changed
        run: |
          git config --global user.name 'GitHub Actions'
          git config --global user.email 'actions@github.com'
          git add public/data/github-stats.json
          git diff --quiet && git diff --staged --quiet || git commit -m "chore: update GitHub stats"
          git push

Wrapping Up

With all of these pieces in place, I now have a comprehensive GitHub stats dashboard that:

  1. Shows my total contributions (608 in the last year)
  2. Displays contributions across my three accounts
  3. Breaks down my activity by type (commits: 96%, PRs: 2%, reviews: 2%)
  4. Visualizes my commit calendar similar to GitHub's interface
  5. Highlights my most-used languages and featured repositories

The best part? It updates automatically every day, so I always have the most current view of my GitHub activity.

By following this guide, you can create a similar dashboard for your own GitHub profiles, even if you contribute through multiple accounts. This approach gives a much more comprehensive view of your contributions than what's visible on a single GitHub profile page.

Have questions or suggestions? Feel free to reach out or submit a PR to improve this solution!