Visualize GitHub Contributions In 3D With Svelte
Published Aug 30, 2023
Table of Contents
- GitHub Skyline
- Project Setup
- Scene Setup
- The API
- Showing The Contributions
- Changing The Colors
- Make It Look More Interesting
- Animating The Contributions
GitHub Skyline
In this post Iโm going to show you how to visualize your GitHub contributions graph in 3D using Threlte inspired by GitHub Skyline.
You can find the code on GitHub.
Project Setup
The easiest way to get started is using the Threlte CLI which is going to scaffold a regular SvelteKit project with Threlte.
npm create threlte
Select the @threlte/extras
optional package for some helpful utilities. The Threlte CLI is also going to install three
and @types/three
because it uses Three.js.
Install the dependencies with npm i
if you skipped it during the setup and start the development server with npm run dev
.
Iโm going to start from scratch and remove everything inside lib
and routes
.
Scene Setup
Inside routes/+page.svelte
Iโm going to set up a 3D scene.
<script lang="ts">
import { Canvas } from '@threlte/core'
import Scene from './scene.svelte'
</script>
<div class="scene">
<Canvas>
<Scene />
</Canvas>
</div>
<style>
.scene {
position: absolute;
inset: 0;
background-color: hsl(200 10% 10%);
}
</style>
The API
You donโt have to read it but in another post on using SvelteKit for web scraping we created a GitHub contributions API for this exact purpose.
Here is the GitHub repository which you can host yourself in case the API ever goes down, or create an API endpoint in SvelteKit and copy the code.
Showing The Contributions
I want to get the types out of the way, which you can ignore if youโre not using TypeScript.
type Day = {
count: number
day: number
level: number
month: string
name: string
year: number
}
export type Contributions = Array<Day | null>
Iโm going to fetch the data on the client because thereโs no need for server-side rendering.
<script lang="ts">
import { onMount } from 'svelte'
import type { Contributions } from '$lib/types'
let contributions: Contributions[] = []
onMount(async () => {
const response = await fetch('https://gh-contributions-api.vercel.app/mattcroat/2022')
contributions = await response.json()
})
</script>
Iโm going to add a grid, camera, lights, and iterate over the contribution rows to render a cube for each day.
Everything in Threlte extends Three.js from the <T>
component.
<script lang="ts">
import { onMount } from 'svelte'
import { T } from '@threlte/core'
import { Align, Grid, OrbitControls } from '@threlte/extras'
import type { Contributions } from '$lib/types'
let contributions: Contributions[] = []
onMount(async () => {
const response = await fetch('https://gh-contributions-api.vercel.app/mattcroat/2022')
contributions = await response.json()
})
</script>
<Grid
infiniteGrid
sectionColor="#4a4b4a"
sectionSize={20}
cellSize={20}
fadeDistance={400}
/>
<T.PerspectiveCamera makeDefault position={[10, 100, 100]} fov={60}>
<OrbitControls enableDamping autoRotate />
</T.PerspectiveCamera>
<T.AmbientLight color="#fff" intensity={0.4} />
<T.DirectionalLight position={[0, 200, 200]} intensity={2} color="#fff" />
<T.DirectionalLight position={[0, 200, -200]} color="#fff" intensity={2} />
<Align auto position.y={100}>
{#each contributions as row, i}
{#each row as day, j}
{#if day}
<T.Group position={[0, 0, 12 * i]}>
<T.Mesh position={[12 * j, day.count / 2, 0]}>
<T.BoxGeometry args={[10, day.count, 10]} />
<T.MeshStandardMaterial color="#fff" />
</T.Mesh>
</T.Group>
{/if}
{/each}
{/each}
</Align>
Changing The Colors
Right now the cubes are white, but Iโm going to create a getColor()
function to get the color based on the day level from the API.
<script lang="ts">
function getColor(level: number) {
switch (level) {
case 0:
return '#0e0e0e'
case 1:
return '#00442a'
case 2:
return '#006d35'
case 3:
return '#00a648'
case 4:
return '#00d35c'
}
}
</script>
<!-- ... -->
<Align auto position.y={40}>
{#each contributions as row, i}
{#each row as day, j}
{#if day}
{@const color = getColor(day.level)}
<T.Group position={[0, 0, 12 * i]}>
<T.Mesh position={[12 * j, y / 2, 0]}>
<T.BoxGeometry args={[10, y, 10]} />
<T.MeshStandardMaterial {color} />
</T.Mesh>
</T.Group>
{/if}
{/each}
{/each}
</Align>
Make It Look More Interesting
I want to make the visualization more interesting by having a base height for the contributions, multiply the existing height for more visual interest, and set a limit for the height.
<script lang="ts">
function normalize(count: number, base = 4, offset = 2) {
switch (true) {
case count === 0:
return base
case count > 40:
return count
default:
return count * (base + offset)
}
}
</script>
<!-- ... -->
<Align auto position.y={40}>
{#each contributions as row, i}
{#each row as day, j}
{#if day}
{@const color = getColor(day.level)}
{@const y = normalize(day.count)}
<T.Group position={[0, 0, 12 * i]}>
<T.Mesh position={[12 * j, y / 2, 0]}>
<T.BoxGeometry args={[10, y, 10]} />
<T.MeshStandardMaterial color={getColor(day.level)} />
</T.Mesh>
</T.Group>
{/if}
{/each}
{/each}
</Align>
Animating The Contributions
To animate the cube height we can use the tweened
store from Svelte and interpolate the scale.y
value from 0 to 1.
If you try and set the scale on the mesh itself itโs going to scale from the center, and for that reason we set it on the group.
<script lang="ts">
import { onMount } from 'svelte'
import { tweened } from 'svelte/motion'
import { quadInOut } from 'svelte/easing'
import { T } from '@threlte/core'
import { Align, Grid, OrbitControls } from '@threlte/extras'
import type { Contributions } from '$lib/types'
const scaleY = tweened(0, { duration: 2000, easing: quadInOut })
onMount(() => {
$scaleY = 1
})
</script>
<!-- ... -->
<Align auto position.y={40}>
{#each contributions as row, i}
{#each row as day, j}
{#if day}
{@const color = getColor(day.level)}
{@const y = normalize(day.count)}
<T.Group position={[0, 0, 12 * i]} scale.y={$scaleY}>
<T.Mesh position={[12 * j, y / 2, 0]}>
<T.BoxGeometry args={[10, y, 10]} />
<T.MeshStandardMaterial color={getColor(day.level)} />
</T.Mesh>
</T.Group>
{/if}
{/each}
{/each}
</Align>
Thatโs it! ๐