Using Chart.js’s legendCallback and generateLegend() with React hooks

There’s not much information on how to use the ‘legendCallback’ option to generate a custom legend, especially with React functional components/hooks. I’m going to show you how to do it!

I needed to create a custom legend to address discrepancies in size between two side-by-side Doughnut charts with different amounts of data. The charts kept rendering with different sizes, and after some Google-ing I decided the best way to solve this would be to generate my own legends.

To make your own custom legend, you need to make sure that you don’t display the default legend, and then provide your chart with a callback function in its options object:

In my case, I wanted to create a simple legend that shows each of my labels next to the color block that represents it in the chart, so this is what the callback looks like:

I’m just using some classic JSX to iterate over my chart’s labels and create a <li> for each label with a color block and its corresponding text. I’m also giving each item an interactive click handler. So far so good!

The tricky part for me was the next step. Chart.js doesn’t automatically render this legend for you — you need to call generateLegend() on your chart instance in order for the legend to actually render in the DOM. So I needed to access the chart’s DOM node in order to call generateLegend(). My immediate thought (and probably yours too) was, “This sounds like a great use case for React’s useRef hook!”, and I was right, sort of.

Unfortunately, I found that the chart legend still wasn’t rendering 100% of the time. Eventually I realized that I had created a race condition where if the chart’s DOM node didn’t exist yet, the legend wouldn’t render. “Fine,” I thought, “I’ll just use React’s useEffect hook, use the chart’s ref as a dependency, and update the legend whenever the ref changes.” But I was foiled again. I soon found out that you can’t use a ref as a dependency for useEffect. Here’s an explanation of why not to use a ref as a dependency in a useEffect call, from the React docs: “…an object ref doesn’t notify us about changes to the current ref value.” So it follows that the useEffect will never get called if we never get notified about changes to the ref value.

Instead, React recommends that you use a different hook — useCallback — to notify us whenever the ref changes. Now we can call generateLegend() inside of this callback. Then, instead of giving your chart instance’s ref attribute a standard ref object created from useRef, we give it that callback we just defined. When the DOM node changes, it will call useCallback, our legend will be generated, and all will be well! Here’s what it looks like, and note that I’m using the useState hook as well in order to store state for the DOM node and our legend:

the useCallback() function
The `Doughnut` component with our callback function and `legend` being rendered underneath.

One last thing: I wanted my legend to be interactive, similar to Chart.js’s default legend. As I mentioned above, I passed a click handler to each <li> in my legend callback. The default Chart.js legend responds to clicks by crossing out the legend item and removing it from the chart, creating an interactive chart where the user can choose which data is being visualized. To replicate this behavior, I wrote the following click handler:

custom legend click handler

I simply access the relevant index in the chart instance’s data and toggle it’s ‘hidden’ attribute and then toggle the element’s class name (I use CSS to cross out elements in the “crossed-line” class). Easy as pie (or…doughnuts, in this case? Sorry, I couldn’t help it). Here’s what it looks like!

normal view
with some items crossed out

I hope you enjoyed learning about custom legends in Chart.js! If you want to check out the repo where I implemented these charts, it’s available here: https://github.com/hankthemason/finances-tracker.

Also, if you’re interested in data visualizations, check out some of my other articles:

Build a map of NYC’s police precincts with React & D3, part 1 and part 2