The day I made my GitHub account, I immediately realized the possibilities of the Commit Heatmap being a colorful grid. I wanted to show some personality, like a smiley face or a friendly greeting, welcoming potential employers to my page. Unfortunately, Git had other ideas, and soon my Heatmap looked more like toxic sludge than a welcome mat.
No matter, I have the power of Inspect Element! I'll just manually adjust each cell. After about 10 minutes into this plan, I had restarted 3 times without completing a single letter. It was clear human input is not reliable enough to complete this task. Good thing I know JavaScript and have several hours to burn!
The first step on this project was to redefine the alphabet. The GitHub commit history is just a grid, so I needed an alphabet that can fit in a grid. Credit to @hgcummings on GitHub for this library. It's exactly what I needed, so I copied and pasted the object into my project.
When I was trying to manually change each cell, I got well acquainted with the commit history HTML. The grid is made of 52 groups with 7 <rect>
elements each. Each cell has a "data-level"
attribute between 0 and 4, this determines what shade of green the cell is.
Immediately I noticed a problem. The commit history is grouped vertically, by weeks. But, my font object is sorted horizontally. If I carried on from here my message would come out sideways! I spent a few minutes thinking about how to proceed, how could I reorganize the arrays into the structure I needed? In the end, I decided it was best to just rewrite the data by hand.
While it certainly would be possible to write a sorting algorithm, I think this choice saved me time in the long run. It also gave me the minor performance boost of shipping and running less code. I also noticed many letters of the alphabet have straight vertical lines (26 lines to be exact). Instead of writing [1,1,1,1,1]
several times, I opted to define it as a variable to be reused. This saved me a small amount of time.
let message = prompt("What do you want to say?", "")
message
.toUpperCase()
.split("")
.map((character) => Letters[character])
//this prompts the user for an input
//then turns that input into an array of pixels
I finally had all the pieces I needed. All that's left to do is plug the user's input into the commit history's grid. This task is easier said than done. The commit history is an array of arrays of cells, and the user message is an array of arrays of pixels. It seems simple in theory, but connecting the right cell with the right pixel is a bit complicated.
My first idea was to access each cell by its "data-date"
attribute. This solution is probably the simplest, but ultimately flawed. The commit history changes each week when a new row is added, eventually, the cell I was accessing would no longer exist. Making the text scroll would be incredibly complicated, computing which cell is where in the grid and how many days are between adjacent cells. This solution quickly was abandoned.
To solve this, I had to think of programming in the most basic terms. What I like to think all programming is at its core is Data Manipulation. HTML is just data, and JavaScript is just a way to manipulate data. With this mindset, I could make a plan.
Visualizing the data like this helps me to conceptualize how to connect it. In this format you can clearly see how each grid cell has a corresponding pixel value derived from the user message. After understanding the data structure, all that's left is writing the code.
const writeToBillboard = (message, rowIndex) =>
clearBoard();
let currentWeek = rowIndex;
message
.toUpperCase()
.split("")
.map((character) => Letters[character])
.map((pixelLetter, index) => {
pixelLetter.map((pixelLine, index) => {
let week = weeks[currentWeek];
pixelLine.map((pixel, index) => {
if (currentWeek >= 0 && currentWeek < 53) {
if (Array.from(week.children)[index + 1]) {
Array.from(week.children)[index + 1].setAttribute(
"data-level",
pixel ? "4" : "0"
);
}
}
});
//move to next row
currentWeek += 1;
});
//skip a line after a letter is complete
currentWeek += 1;
});
};{
First, we convert the user input string into an array of pixels. Next, we access each pixel letter, then each pixel line. Then we define the week
as the current row of cells we're accessing. After that, we access each cell, and we're ready to overwrite the data! We do a couple of checks to make sure we're on a row that exists and a cell that exists (otherwise it throws about 30 errors per second), then we set the cell's data-level
attribute to a 4 or 0, depending on the pixel's value. Finally! We've put a message on our commit history!
The hard part is over, but it's not quite ready to push yet. We still need to make it scroll. This is simpler than it sounds. With setInterval()
we can call writeToBillboard()
every 100ms, and increment the rowIndex
down one each time. This writes the message one row to the left ten times per second, giving the illusion of a moving image.
const scrollWrite = () =>
let rowIndex = 53;
let interval = setInterval(() => {
writeToBillboard(message, rowIndex)
//check if rowIndex has extended far enough for all
//characters
rowIndex < 0 - message.split("").length * 4
? //if true: reset
(rowIndex = 53)
: //if false: keep going
(rowIndex -= 1);
}, 100);
};{
We define rowIndex
as 53 to start at the row furthest to the right. We call writeToBillboard()
, passing in the message and rowIndex
as arguments. After the message is written, we check if all characters have passed the left side of the grid. Each character takes 4 rows to write, including the gap between characters. So by subtracting the number of characters multiplied by 4, we know what rowIndex
will be far enough to have passed all characters. If rowindex
has passed this number, we reset rowIndex
to 53, if not we subtract one so the message moves a little bit to the left.
The final step in the process is to turn our code into a Chrome Extension. Chrome extensions require a manifest.json
to define the properties of the code and let Chrome know what to do with it. We have to name the extension, version it, and give it a description. In "content_scripts"
, we tell the extension to run content.js when on any github.com page.
{
"manifest_version": 3,
"name": "GitHub Billboard",
"version": "1.0.0",
"description": "Turn your GitHub commits into a marquee billboard",
"icons": {
"32": "icon32.png",
"128": "icon128.png"
},
"content_scripts": [
{
"js": ["content.js"],
"matches": ["https://github.com/*"]
}
]
}
And with that, we're ready to push our code, you can check it out here. This project was a lot of fun to work on and I was amazed at the power and simplicity of Chrome Extensions. I was impressed by how quickly I was able to get up and running with a custom extension. I'll be making more in the future!
Top comments (2)
This was a fun read! I really enjoyed your perspective of programming being data manipulation and how you visualized your problem!
I'm still trying to grasp JS and I think visualizing the task at hand would help. Did any resource help you do visualize programming tasks or did that ability come with practice?
I'm glad you enjoyed it, thanks for the feedback!
I'm a visual learner, so "seeing" information is the easiest way for me to understand it. You can use
console.log()
to log anything and look at it in the console, this allowed me to get a clear picture of the data structure. Working on this project, I had aconsole.log()
on literally every other line! I also used Figma to make the infographic demonstrating the data structures.So use your tools to understand your task as best you can. And make very generous use of
console.log()
!