Skip to content

mahdihasnat/contribution-art-maker

Repository files navigation

contribution-art-maker

Attempt to create nice contribution art

Motivation

I came accross to a linkdein post where an author shared nice doller sign in his github contribution chart.

image

This caught my eyes and being a git enthusiast, I was wondering if I could also draw some art here. This will be very long and painfull process (maybe not that painfull but preparing for worst).

Idea

As per contribution calculation logic link, I can just create commits for specific date and contribution points will be added on that day (utc only).

Now I can just run a cron job everyday. That job will commit appropriate number of commit in some private repo. But after just one google search, I found a repo that generate contribution in the past! github-activity-generator

I am not surprised because it is possible to spoof commit data. I had a similar experiment here childhood-codes. I just created a commit for year 2004.

Contribution graph is generated for a year. There are 7 rows. Row starts at sunday. There will be 365[+1 if leap year] data points. Each cell can contain one of the 5 different color. From black to green.

So my contribution for year 2017 is completely empty: image

I want to find the formula how github calculates the weight of individual cells. To test that I am going to add 1 commit in first day, 2 commit in second day and so on for 365 days of 2017. Yes I pushed 66,795 commits to my private repo. But apparently github only counted last 1000 commits for 2017. image

I was wondering where is the color level decision taken? I checked the network tab and saw this tag comes from server. So the decision code is in the server.

image

I will just need to test for few days. So now I will test for 43 days. There will be (1 + 2 + 3 + .. + 43) = 43 * 44 / 2 = 43 * 22 = 946 contributions [ yay it is less than 1000]. I tested with few fix max contributions. (eg. 34, 40, 41, 42, 43).

image

Level Range (out of 34) Total
Level-1 1 .. 8 8
Level-2 9 .. 17 9
Level-3 18 .. 25 8
Level-4 26 .. 34 9

image

Level Range (out of 40) Total
Level-1 1 .. 10 10
Level-2 11 .. 20 10
Level-3 21 .. 30 10
Level-4 31 .. 40 10

image

Level Range (out of 41) Total
Level-1 1 .. 10 10
Level-2 11 .. 20 10
Level-3 21 .. 30 10
Level-4 31 .. 41 11

image

Level Range (out of 42) Total
Level-1 1 .. 10 10
Level-2 11 .. 21 11
Level-3 22 .. 31 10
Level-4 32 .. 42 11

image

Level Range (out of 43) Total
Level-1 1 .. 10 10
Level-2 11 .. 21 11
Level-3 22 .. 32 11
Level-4 33 .. 43 11

image

Level Range (out of 44) Total
Level-1 1 .. 11 11
Level-2 12 .. 22 11
Level-3 23 .. 33 11
Level-4 34 .. 44 11

After analyzing these results I came up with a function to get levels based on contribution.

function getLevel(contribution, maxContribution) {
const segmentSize = Math.floor(maxContribution / 4)
const segments = [segmentSize, segmentSize, segmentSize, segmentSize];
var reminder = maxContribution % 4;
if (reminder > 0) {
segments[3]++;
reminder--;
}
if(reminder > 1) {
segments[2] ++;
reminder--;
}
if(reminder > 0) {
segments[1] ++;
reminder--;
}
console.assert(reminder === 0);
for(let i = 0; i < segments.length; i++) {
if(contribution <= segments[i]) {
return i + 1;
}
contribution -= segments[i];
}
console.assert(false);
return -1;
}

Though this formula works for ideal cases I generated, but I really should test this formula for other existing pages. So here is a validator function that visits all the cells and validate the level with my implementation.

function getLevel(contribution, maxContribution) {
const segmentSize = Math.floor(maxContribution / 4)
const segments = [segmentSize, segmentSize, segmentSize, segmentSize];
var reminder = maxContribution % 4;
if (reminder > 0) {
segments[3]++;
reminder--;
}
if(reminder > 1) {
segments[2] ++;
reminder--;
}
if(reminder > 0) {
segments[1] ++;
reminder--;
}
console.assert(reminder === 0);
for(let i = 0; i < segments.length; i++) {
if(contribution <= segments[i]) {
return i + 1;
}
contribution -= segments[i];
}
console.assert(false);
return -1;
}
const table = document.getElementsByClassName('js-calendar-graph-table')[0]
const cells = table.querySelectorAll('[data-date],[data-level]')
const levelAndContribution = []
cells.forEach(cell => {
const date = cell.getAttribute('data-date');
const level = parseInt(cell.getAttribute('data-level'));
const toolTipId = cell.getAttribute('aria-labelledby');
const toolTip = document.getElementById(toolTipId);
const toolTipText = toolTip.textContent;
if(level > 0) {
const contribution = parseInt(toolTipText.match(/(\d+) contribution/)[1]);
levelAndContribution.push({level, contribution, date});
}
});
const maxContribution = Math.max(...levelAndContribution.map(item => item.contribution));
console.log('Max contribution:', maxContribution);
levelAndContribution.forEach(({level, contribution, date}) => {
let expectedLevel = getLevel(contribution, maxContribution);
console.assert(level === expectedLevel, `Expected level ${expectedLevel}, got ${level} for contribution ${contribution} on date ${date}`);
});
console.log('All assertions passed');

I was hoping that this will pass for every year, but alas!. There are exceptions in some year. For example in 2018, I have 3 days with [4,4,1] contributions. Here my formula suggests the levels will be [4,4,2]. But actual levels were [4,4,4]. 🤦

Anyway, I am going to stick to my formula hoping that it will be correct for majority of the cases.

First time when I tried to color every 365 days, I just got color for last few days. To be exact , contribution for last 1000 comiits. So What if instead of single push after all commits, I will push after every 1000 commit. I tried and actually worked! 🎉

image

Now I got the secret to arbitrarily add any number of contribution to the year.

image

This is my current contribution chart for year 2020. There are different contribution for each day. I want to erase all the existing contribution from this chard. Obviously I don't want to temper the commit date of my existing codes. One idea is to make extra commit on each day such that every day has same number of commits. So I tried that with a little bit of script as the following. You can run this script on dev-tools/console on your github profile page and selecting proper year. After you run the js script, a bash script will be downloaded. Just run the bash script at any of your repository. Tip: you should create a private repository so that you can undoe the contributions by deleteing the repository.

const table = document.getElementsByClassName('js-calendar-graph-table')[0];
const cells = table.querySelectorAll('[data-date],[data-level]');
const dateAndContribution = {};
cells.forEach(cell => {
const date = cell.getAttribute('data-date');
const level = parseInt(cell.getAttribute('data-level'));
const toolTipId = cell.getAttribute('aria-labelledby');
const toolTip = document.getElementById(toolTipId);
const toolTipText = toolTip.textContent;
if(level > 0) {
const contribution = parseInt(toolTipText.match(/(\d+) contribution/)[1]);
dateAndContribution[date] = contribution;
}
});
const maxContribution = Math.max(...Object.values(dateAndContribution));
const minDate = Object.keys(dateAndContribution).reduce((minDate, date) =>
date < minDate ? date : minDate, Object.keys(dateAndContribution)[0]);
const year = parseInt(minDate.split('-')[0]);
console.log('Detected year:', year);
console.log('MaxContribution:', maxContribution);
const startDate = new Date(Date.UTC(year, 0, 1));
const endDate = new Date(Date.UTC(year, 11, 31)) ;
function addDays(date, days) {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
}
let currentDate = startDate;
let fileContent = '';
var commitsToPush = 0
while (currentDate <= endDate) {
const dateString = currentDate.toISOString().split('T')[0];
const existingContribution = dateAndContribution[dateString] || 0;
const moreContribution = maxContribution - existingContribution;
const isoDateString = currentDate.toISOString()
for (let i = 0; i < moreContribution; i++) {
const env_Vars = `GIT_AUTHOR_DATE="${isoDateString}" GIT_COMMITTER_DATE="${isoDateString}"`
const commit_msg = `Commit ${ existingContribution + i + 1} for day ${dateString}`;
fileContent += `${env_Vars} git commit --allow-empty -m "${commit_msg}"\n`;
commitsToPush++
if (commitsToPush >= 1000) {
fileContent += 'git push\n';
commitsToPush = 0;
}
}
currentDate = addDays(currentDate, 1);
}
fileContent += 'git push\n';
// Create a Blob with the file content
const blob = new Blob([fileContent], { type: 'text/plain' });
// Create a temporary URL for the Blob
const url = URL.createObjectURL(blob);
// Create a hidden anchor element
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = 'commits.sh'; // Set the desired filename (changed to .sh for shell script)
// Trigger a click on the anchor element to start the download
document.body.appendChild(a);
a.click();
// Clean up
document.body.removeChild(a);
URL.revokeObjectURL(url);

image

Voila! what a nice green valley 💚

I was wondering what can I do with this superpower I have! Should I just draw my crush name in the chart and ask for a date (eh too simp). But Certainly I can do better. Why not draw "Hello World"!

How so? Say I have an ascii art of the text. There will be 7*54 grid. Consider the grid as binary colored. We have an existing contribution chart, each cell is labeled from 0 to 4. Say current max contribution is mx. For the colored cell in art grid, we will have 4*mx contributions for that day. For uncolored cell in art grid, if there was any contribution on that day previously we can have mx contribution, otherwise we can leave that day without any contributions. While doing this, I felt satisfied pulling generator trick in python.

image

The result is so satisfying! You are welcome to check this out in my github profile.

Collaboration

This is currently work in progress project. If you find it interesting and want to contribute, you can create issues, pull requests or just email/message me for discussion.

About

Attempt to create nice contribution art

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published