seanmcp.com

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 (
  1. {year}
    {articlesLabel} {notesLabel}
  2. ); }) }

There is certainly room for improvement, but I'm happy with how it turned out. Here it is in all its glory:

Happy coding!

Reply by email Romans 5:8