Public

GitHub Contribution Graph Waveform

Turned a static GitHub contribution graph into a beat-synced waveform visualizer — Claude inspected the DOM via Chrome, built a reusable animator with wave/pulse/rain effects, then synced it to audio beats for a developer video intro.

Stats
Prompts
25
Tool calls
42
Messages
196
Assets
6
Meta
Author
@aarondfrancis
Created At
February 08, 2026
Source
Claude
Share
Permalink

im recording a video for developers and I want to use the github contribution graph as a fun flair element. I want you to write some javascript to control it. can you use your chrome skill to figure out how to do that? we're basically going to alternate the green graph to make an animation. lets start by seeing if you can find the graph on github.com/aarondfrancis

Permalink
Thought 1 time. Used 0 tool calls. +0 -0 [Expand]
Thinking

The user wants me to help create a JavaScript animation for the GitHub contribution graph. They want to use it as a visual flair element in a developer video. Let me start by checking the browser context and navigating to the GitHub profile to inspect the contribution graph.

First, I should get the browser tab context.

Permalink

I'll check the browser context and navigate to that GitHub profile to inspect the contribution graph structure.

Permalink
Thought 4 times. Used 4 tool calls. +0 -0 [Expand]
Tool: mcp__claude-in-chrome__tabs_context_mcp
mcp__claude-in-chrome__tabs_context_mcp
{
  "createIfEmpty": true
}
{"availableTabs":[{"tabId":1857700089,"title":"New Tab","url":"chrome://newtab"}],"tabGroupId":1913878167}
Thinking

Good, I have a new tab available. Let me navigate to the GitHub profile page to see the contribution graph.

Tool: mcp__claude-in-chrome__navigate
mcp__claude-in-chrome__navigate
{
  "url": "https://github.com/aarondfrancis",
  "tabId": 1857700089
}
Navigated to https://github.com/aarondfrancis
Thinking

Let me wait a moment for the page to load and then take a screenshot to see the contribution graph.

Tool: mcp__claude-in-chrome__computer
mcp__claude-in-chrome__computer
{
  "action": "wait",
  "duration": 2,
  "tabId": 1857700089
}
Waited for 2 seconds
Thinking

Good, the page loaded. Now let me take a screenshot to see the contribution graph and then inspect the DOM structure to understand how to manipulate it.

Tool: mcp__claude-in-chrome__computer
mcp__claude-in-chrome__computer
{
  "action": "screenshot",
  "tabId": 1857700089
}
Successfully captured screenshot (1584x776, jpeg) - ID: ss_7715dtw5m
Thinking

I can see the GitHub contribution graph. It shows "3,662 contributions in the last year" with the green squares. Now I need to inspect the DOM structure to understand how the graph is built so I can manipulate it with JavaScript.

Let me use the read_page tool to find the contribution graph elements, or I can use JavaScript to inspect it directly. Let me try finding the contribution graph structure.

Permalink

I can see the contribution graph. Now let me inspect the DOM structure to understand how it's built.

Permalink
Thought 1 time. Used 1 tool call. +0 -0 [Expand]
Tool: mcp__claude-in-chrome__javascript_tool
mcp__claude-in-chrome__javascript_tool
{
  "action": "javascript_exec",
  "tabId": 1857700089,
  "text": "\n// Find the contribution graph and inspect its structure\nconst graph = document.querySelector('.js-calendar-graph');\nconst cells = document.querySelectorAll('.ContributionCalendar-day');\n\n// Get sample cell structure\nconst sampleCell = cells[10];\nconst cellInfo = {\n  totalCells: cells.length,\n  graphFound: !!graph,\n  sampleCell: sampleCell ? {\n    tagName: sampleCell.tagName,\n    className: sampleCell.className,\n    attributes: Array.from(sampleCell.attributes).map(a => ({name: a.name, value: a.value})),\n    style: sampleCell.getAttribute('style'),\n    innerHTML: sampleCell.innerHTML.substring(0, 200)\n  } : null,\n  // Check if they use data attributes or fill colors\n  fills: Array.from(new Set(Array.from(cells).map(c => c.getAttribute('data-level')))).slice(0, 10)\n};\n\nJSON.stringify(cellInfo, null, 2);\n"
}
{
  "totalCells": 372,
  "graphFound": true,
  "sampleCell": {
    "tagName": "TD",
    "className": "ContributionCalendar-day",
    "attributes": [
      {
        "name": "tabindex",
        "value": "-1"
      },
      {
        "name": "data-ix",
        "value": "10"
      },
      {
        "name": "aria-selected",
        "value": "false"
      },
      {
        "name": "aria-describedby",
        "value": "contribution-graph-legend-level-1"
      },
      {
        "name": "style",
        "value": "width: 10px"
      },
      {
        "name": "data-date",
        "value": "2025-03-23"
      },
      {
        "name": "id",
        "value": "contribution-day-component-0-10"
      },
      {
        "name": "data-level",
        "value": "1"
      },
      {
        "name": "role",
        "value": "gridcell"
      },
      {
        "name": "data-view-component",
        "value": "true"
      },
      {
        "name": "class",
        "value": "ContributionCalendar-day"
    [TRUNCATED]
Thinking

Excellent! I found the structure. The contribution graph uses:

  • td elements with class ContributionCalendar-day
  • A data-level attribute (0-4) that controls the color intensity
  • There are 372 cells total

The data-level attribute values likely map to:

  • 0 = no contributions (gray/empty)
  • 1 = light green
  • 2 = medium green
  • 3 = darker green
  • 4 = darkest green

Let me verify the level values and then create an animation. First, let me see all the unique levels.

Permalink

Found it. The graph uses data-level attributes (0-4) to control the green intensity. Let me verify the levels and see how the colors work.

Permalink
Thought 1 time. Used 1 tool call. +0 -0 [Expand]
Tool: mcp__claude-in-chrome__javascript_tool
mcp__claude-in-chrome__javascript_tool
{
  "action": "javascript_exec",
  "tabId": 1857700089,
  "text": "\n// Get all unique levels and count cells by level\nconst cells = document.querySelectorAll('.ContributionCalendar-day');\nconst levelCounts = {};\ncells.forEach(cell => {\n  const level = cell.getAttribute('data-level');\n  levelCounts[level] = (levelCounts[level] || 0) + 1;\n});\n\n// Get the computed styles for different levels\nconst sampleCells = {};\ncells.forEach(cell => {\n  const level = cell.getAttribute('data-level');\n  if (!sampleCells[level]) {\n    const style = window.getComputedStyle(cell);\n    sampleCells[level] = {\n      backgroundColor: style.backgroundColor,\n      level: level\n    };\n  }\n});\n\nJSON.stringify({ levelCounts, sampleCells }, null, 2);\n"
}
{
  "levelCounts": {
    "0": 4,
    "1": 338,
    "2": 21,
    "3": 7,
    "4": 2
  },
  "sampleCells": {
    "0": {
      "backgroundColor": "rgb(239, 242, 245)",
      "level": "0"
    },
    "1": {
      "backgroundColor": "rgb(172, 238, 187)",
      "level": "1"
    },
    "2": {
      "backgroundColor": "rgb(74, 194, 107)",
      "level": "2"
    },
    "3": {
      "backgroundColor": "rgb(45, 164, 78)",
      "level": "3"
    },
    "4": {
      "backgroundColor": "rgb(17, 99, 41)",
      "level": "4"
    }
  }
}
Thinking

Now I have the full picture:

  • Level 0 = gray (no contributions)
  • Level 1-4 = increasingly dark green

The cells use data-level to control the color via CSS. I can create animations by changing the data-level attribute on each cell.

Let me create a simple wave animation as a proof of concept - alternating the levels to create a visual effect.

Permalink

Now I understand the structure. Let me create a simple wave animation as a proof of concept.

Permalink
Thought 2 times. Used 2 tool calls. +0 -0 [Expand]
Tool: mcp__claude-in-chrome__javascript_tool
mcp__claude-in-chrome__javascript_tool
{
  "action": "javascript_exec",
  "tabId": 1857700089,
  "text": "\n// Save original levels so we can restore later\nconst cells = Array.from(document.querySelectorAll('.ContributionCalendar-day'));\nwindow.originalLevels = cells.map(c => c.getAttribute('data-level'));\n\n// Create a wave animation\nlet frame = 0;\nwindow.waveAnimation = setInterval(() => {\n  cells.forEach((cell, i) => {\n    // Get column index for wave effect\n    const ix = parseInt(cell.getAttribute('data-ix') || i % 53);\n    // Create wave based on column position and frame\n    const wave = Math.sin((ix + frame) * 0.3);\n    // Map wave (-1 to 1) to level (0 to 4)\n    const level = Math.round((wave + 1) * 2);\n    cell.setAttribute('data-level', level);\n  });\n  frame++;\n}, 100);\n\n\"Wave animation started! Run: clearInterval(window.waveAnimation) to stop\";\n"
}
Wave animation started! Run: clearInterval(window.waveAnimation) to stop
Thinking

The animation should be running now. Let me take a screenshot to show the user what it looks like.

Tool: mcp__claude-in-chrome__computer
mcp__claude-in-chrome__computer
{
  "action": "screenshot",
  "tabId": 1857700089
}
Successfully captured screenshot (1584x776, jpeg) - ID: ss_551035bub
Thinking

You can see the wave animation is running - the green squares are now showing a wave pattern moving across the graph. The screenshot captures one frame of the animation. Let me stop the animation and give the user the code they can use.

Permalink

The wave animation is running. You can see the pattern has changed from the original - it's now showing alternating bands of green intensity moving across the graph.

Let me stop this and give you the clean code you can use:

Permalink
Thought 1 time. Used 1 tool call. +0 -0 [Expand]
Tool: mcp__claude-in-chrome__javascript_tool
mcp__claude-in-chrome__javascript_tool
{
  "action": "javascript_exec",
  "tabId": 1857700089,
  "text": "\n// Stop and restore original\nclearInterval(window.waveAnimation);\n\n// Restore original levels\nconst cells = Array.from(document.querySelectorAll('.ContributionCalendar-day'));\nif (window.originalLevels) {\n  cells.forEach((cell, i) => {\n    cell.setAttribute('data-level', window.originalLevels[i]);\n  });\n}\n\n\"Animation stopped, original graph restored\";\n"
}
Animation stopped, original graph restored
Thinking

I've successfully found and manipulated the GitHub contribution graph. Now I can provide the user with a clean code snippet they can use. The key findings are:

  1. Cells are td.ContributionCalendar-day elements
  2. Color is controlled by data-level attribute (0-4)
  3. 0 = gray, 1-4 = light to dark green
  4. There are ~372 cells
  5. Each cell has a data-ix attribute for column position

I should give the user a nice reusable code snippet they can paste into the console.

Permalink

Here's what I found:

Key structure:

  • Cells: td.ContributionCalendar-day elements (372 total)
  • Color control: data-level attribute (0=gray, 1-4=light→dark green)
  • Column position: data-ix attribute (useful for wave effects)

Here's clean, reusable code you can paste in the console:

// GitHub Contribution Graph Animator
const ContribAnim = {
  cells: [],
  original: [],
  interval: null,
  
  init() {
    this.cells = Array.from(document.querySelectorAll('.ContributionCalendar-day'));
    this.original = this.cells.map(c => c.getAttribute('data-level'));
    return this;
  },
  
  // Wave animation
  wave(speed = 100) {
    let frame = 0;
    this.interval = setInterval(() => {
      this.cells.forEach((cell, i) => {
        const ix = parseInt(cell.getAttribute('data-ix') || i % 53);
        const level = Math.round((Math.sin((ix + frame) * 0.3) + 1) * 2);
        cell.setAttribute('data-level', level);
      });
      frame++;
    }, speed);
  },
  
  // Pulse all cells
  pulse(speed = 200) {
    let frame = 0;
    this.interval = setInterval(() => {
      const level = Math.round((Math.sin(frame * 0.5) + 1) * 2);
      this.cells.forEach(cell => cell.setAttribute('data-level', level));
      frame++;
    }, speed);
  },
  
  // Random sparkle
  sparkle(speed = 50) {
    this.interval = setInterval(() => {
      this.cells.forEach(cell => {
        cell.setAttribute('data-level', Math.floor(Math.random() * 5));
      });
    }, speed);
  },
  
  stop() {
    clearInterval(this.interval);
    this.cells.forEach((c, i) => c.setAttribute('data-level', this.original[i]));
  }
};

// Usage:
ContribAnim.init().wave();     // Start wave
// ContribAnim.init().pulse();  // Or pulse
// ContribAnim.init().sparkle(); // Or random sparkle
// ContribAnim.stop();          // Stop and restore

Want me to demo any other animation styles, or tweak the speed/pattern?

Permalink

~/Desktop/4k.m4a i'd like you to examine this audio file and see if you can write something that syncs to that

Permalink

Let me examine that audio file to understand its characteristics.

Permalink
Thought 0 times. Used 1 tool call. +0 -0 [Expand]
$ file "~/Desktop/4k.m4a" && afinfo "~/Desktop/4k.m4a"
~/Desktop/4k.m4a: ISO Media, Apple iTunes ALAC/AAC-LC (.M4A) Audio
File:           ~/Desktop/4k.m4a
File type ID:   m4af
Num Tracks:     1
----
Data format:     2 ch,  48000 Hz, aac  (0x00000000) 0 bits/channel, 0 bytes/packet, 1024 frames/packet, 0 bytes/frame
                no channel layout.
estimated duration: 8.199958 sec
audio bytes: 259323
audio packets: 387
bit rate: 251281 bits per second
packet size upper bound: 1350
maximum packet size: 1350
audio data file offset: 61440
optimized
audio 393598 valid frames + 2112 priming + 578 remainder = 396288
format list:
[ 0] format:	  2 ch,  48000 Hz, aac  (0x00000000) 0 bits/channel, 0 bytes/packet, 1024 frames/packet, 0 bytes/frame
Channel layout: Stereo (L R)
----
Permalink

Let me try using ffmpeg or sox to analyze the audio and potentially extract beat information.

8.2 second audio file. Let me analyze it for beats/rhythm - I'll try to extract some tempo information.

Loading more...