Create a "Dave Rupert"-inspired activity graph
This article was written when seanmcp.com was powered by Astro. I have left the content in place, but the activity graph described no longer renders.
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 spans 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:
  {
    Object.entries(yearCount).map(([year, { articles, notes }]) => {
      const articlesLabel = `${articles} articles`;
      const notesLabel = `${notes} notes`;
      return (
        - 
          {year}
          
            {articlesLabel}
          
          
            {notesLabel}
          
        );
    })
  }
There is certainly room for improvement, but I'm happy with how it turned out. Here it is in all its glory:
Happy coding!