Despite writing on this site since 2016 (when it was built on Anchor CMS), I haven’t been very prolific. After almost five years, this is only post number 44. I’ve had periods of greater output over the past 18 months but I’m still nowhere near as productive here as I should be considering all of the drafts in my Notes app.
So I thought I’d try to shame myself into it. I’ve added a chart to the Writing section of my home page that visualises how many articles I’ve written. Each month is represented by one block. The blocks are shaded according to how many posts I published that month, sort of like GitHub’s contribution graph.
To build my chart, I first needed to round up some data. My site is rendered server side using Jekyll, which uses the Liquid templating language. I can get my total number of posts with site.posts | size
and I can get each post’s date with post.date
. Using some pretty convoluted Liquid loops, I was able to output the number of posts in each month… but only for months in which I’ve published. Months with no posts weren’t accounted for. I tried a few methods of filling in the zero post month gaps. I was able to figure out whether a post was newer than the last one, but not by how many months — at least not in a way that wasn’t severely overcomplicated. So I finally gave up and turned to JavaScript.
This changed my approach. Instead of relying on post dates to generate the months, I could start with an array of months (finite, since I know the first one is 2016-06) and increase the post count numbers of each while looping through the posts. I plan double back and generate the array programmatically, but for now it just looks like this…
var months = [
{
month: '2020-03',
count: 0
},
{
month: '2020-02',
count: 0
},
...
]
The count
of each month is initially set to 0
. The months are in YYYY-MM format because I display the dates on the homepage in ISO 8601. This allowed me to query the page, get the date of each article, and increment the post count of the associated month. (I was just typing that if I ever want to change the displayed date format, I’ll move the YYYY-MM into a data attribute but it occurs to me that I could do that anyway and save the slicing here.)
// 🚨 jQuery trigger warning 🚨
$('.article-list__date').each(function(index) {
// Get YYYY-MM from article
let yyyymm = $(this).text().slice(0,-3);
// Find the article's month in the array of months
let monthIndex = months.findIndex(element => element.month === yyyymm);
// Increase post count of that month
months[monthIndex].count++;
});
Now my array looks like this:
var months = [
{
month: '2020-03',
count: 4
},
{
month: '2020-02',
count: 0
},
...
]
Next I needed to do some math. I’m using opacity to determine how dark the month blocks should appear, with the default being .chart__month { opacity: 0.1; }
. The month with the most posts should always get an opacity value of 1.0
. So my scale is 0.1—1.0. We can find the highest number in the array of objects using Math.max
.
// Get highest post count for determining opacity
let highestCount = Math.max.apply(Math, months.map(function(o) { return o.count; }));
let minimumOpacity = 0.10;
I calculate each month’s opacity value with this line. I divide the highestCount
by the number of posts in the month and multiply the result by 0.9, or 1 minus our default minimumOpacity
value. Then I add back minimumOpacity
(or 0.1) to make sure months with one post are darker than months with no posts. (If I wanted to use a different style for empty months — maybe completely transparent with just a border or something — I could just do month.count / highestCount
and leave it at that.)
let opacity = ((month.count / highestCount) * (1 - minimumOpacity)) + minimumOpacity;
Lastly, I build out an HTML element for the month and add it to the <div class="chart">
element pre-rendered on the page. Altogether, my loop looks like this:
months.forEach(function(month) {
let opacity = ((month.count / highestCount) * (1 - minimumOpacity)) + minimumOpacity;
$('.chart').append( '<span class="chart__month" style="opacity: ' + opacity + ';" title="' + month.month + ': ' + month.count + ' posts"></span>' );
});
And that’s it. I don’t love that it gets generated client-side, but I do like that it works and, excluding the array, it’s only 11 lines of code. It’s worth noting that jQuery is absolutely not a dependency here — I’ve used it to simplify a few things because it’s already being loaded for other stuff, but all of this could be rewritten in vanilla JS with little complication and I will likely do that when I programmatically generate the initial array.
Do you have a suggestion for how this could be improved? Feel free to email or tweet at me using the links below.