Create a "Dave Rupert"-inspired activity graph
I noticed on Dave Rupert’s website that he had a little activity graph to illustrate how many articles he has written per year over time.
That seemed like a nice little feature, so I decided to try to recreate it for my site. This is a little code walk through how I made it using in a custom Astro component.
First, I needed to grab all of the data to count. I have two main forms of
content on this site, articles and notes. Using Astro’s
glob()
method, I was able to get that data and pass it into a sorting
function:
const articles = getSortedContent(
await Astro.glob("../pages/articles/*.{md,mdx}")
);
const notes = getSortedContent(
await Astro.glob("../../content/notes/*.{md,mdx}")
);
This code is all subject to change, so for the latest checkout the source on GitHub
Next, I knew that I needed to keep a record of some totals, so I created an object to track the totals for each content-type by the year:
const yearCount: Record<number, { articles: number; notes: number }> = {};
With that in place, I could loop through articles
and notes
and start
counting:
articles.forEach((article) => {
const year = new Date(article.frontmatter.pubDate).getFullYear();
if (!yearCount[year]) yearCount[year] = { articles: 0, notes: 0 };
yearCount[year].articles++;
});
notes.forEach((note) => {
const year = new Date(note.frontmatter.pubDate).getFullYear();
if (!yearCount[year]) yearCount[year] = { articles: 0, notes: 0 };
yearCount[year].notes++;
});
This was far from DRY, but I didn’t think it was worth refactoring at this point.
With the data ready, it was time to start rendering. I went with an ordered
list, ol
, with list items for each year. Within each li
, I have a label, and
two span
s to represent articles and notes.
I tried to label things in a helpful manner, but I’m sure there are accessibility improvements to be made.
The last bit of magic was finding a decent height for the most prolific year,
and then use that when calculating the individual span
heights. And thanks to
Math.max()
’s API, this was pretty nice:
const highest = Math.max(
...Object.values(yearCount).map((record) => record.articles + record.notes)
);
// 16 * 4 or 64 is the maximum height for a year
const multiplier = (16 * 4) / highest;
With all that set up, all I needed to do was iterate:
<ol>
{
Object.entries(yearCount).map(([year, { articles, notes }]) => {
const articlesLabel = `${articles} articles`;
const notesLabel = `${notes} notes`;
return (
<li>
<div title={String(articles + notes)}>{year}</div>
<span
class="articles"
style={{ height: articles * multiplier + "px" }}
title={articlesLabel}
>
<span class="--visually-hidden">{articlesLabel}</span>
</span>
<span
class="notes"
style={{ height: notes * multiplier + "px" }}
title={notesLabel}
>
<span class="--visually-hidden">{notesLabel}</span>
</span>
</li>
);
})
}
</ol>
There is certainly room for improvement, but I’m happy with how it turned out. Here it is in all its glory:
- 20183 articles 0 RSS Club articles
- 201919 articles 0 RSS Club articles
- 202053 articles 0 RSS Club articles
- 202162 articles 0 RSS Club articles
- 202261 articles 0 RSS Club articles
- 202373 articles 7 RSS Club articles
- 202463 articles 1 RSS Club articles
Happy coding!