Linked Highlighting with React, D3.js and Reflux
One of the best interaction techniques for data vis is to have linked highlighting between related visualizations. In this post, I share a method for implementing linked highlighting using React, D3.js and Reflux.
A demo of the end result can be seen here:
The code can be found here:
One way to achieve linked highlighting on a group of charts is to have marks displayed on the charts as the user moves their mouse around. It’s a technique I used extensively on my NBA Shot Visualization web site, Buckets, shown below:
While I wrote Buckets using Angular, I’ve since been doing development with React and the Flux architecture via Reflux. In this post, I’ll explain how to use these tools, along with D3.js, to create a couple of basic charts with linked highlighting. The final result is shown below:
I thought I’d try using the Yeoman react-webpack generator to create this example, which worked pretty well, but most files needed to be reformatted in the ES6 style I ended up writing the code in. To get started, I ran:
$ yo react-webpack
This uses the Yeoman generator to create all the initial files needed for the project. I said no to including react-router, yes to including the Reflux library, and used basic CSS as the style language. Note that I also updated React from 0.12 to 0.13 in the generated package.json.
At this point, you can run the development server with the following command:
$ grunt serve
Drawing Basic Charts with React and D3
The first step is to draw some basic charts in the app. I decided to use a really simple line chart and a radial heatmap of the same data. I used Yeoman again to generate starter files:
$ yo react-webpack:component LineChart --es6
$ yo react-webpack:component RadialHeatmap --es6
I had to modify the code style quite a bit, but it was better than starting with nothing.
To begin, I updated the main app file, LinkedHighlightingApp, to use these two components with some randomized data:
Nothing too exciting here. Let’s take a look at making a (very) basic line chart using React and D3.
While typical D3 usage involves manipulating the DOM using d3.select(parent).append(), that style doesn’t give us the benefits of using React to manage the DOM. Luckily, React comes with built-in support for SVG tags, so we can render our line chart using the same JSX code we use for all our other React components. I first learned about this approach at OpenVis 2015 during Ilya Boyandin’s great talk Interactive Datavis with React.
While we’re not using D3's DOM manipulation API, we are still using some valuable D3 functions. We use linear scales to map the data to the dimensions of our component and the d3.svg.line() function to generate the d attribute for the path element. Note that d3.extent returns [min, max] based on the accessor provided.
With the addition of some CSS, we’ll have a simple component that draws a line based on the data provided. I put the CSS for this component in src/styles/LineChart.css based on Yeoman’s provided project structure, although I think I prefer having the CSS files with the JS files now.
Cool, we now have a simple line representing the data.
The radial heatmap component, inspired by SnapShot, is created in a similar fashion. However, instead of creating a single path element to represent the data, we map the data points to circles in our render function and use D3 linear scales to scale the output colour and radius.
Voila, a static radial heatmap.
Adding in Linked Highlighting
At this point, the application renders a static line and a static radial heatmap. To add in linked highlighting behaviour on mouse-over, we’ll make use of Reflux’s Actions and Stores. If you didn’t want to use the Flux architecture, you could just pass callbacks to the components in the standard React way.
We can use Yeoman to generate a couple of stubs to get started:
$ yo react-webpack:action chart --es6
$ yo react-webpack:store chart --es6
(Note that I renamed the created action file from ChartActionCreators to ChartActions.)
We’ll add one action to ChartActions called highlight and a corresponding callback to the ChartStore that broadcasts the data point to be highlighted.
Now, we can update the LinkedHighlightApp to listen for changes to the store and push the data point to be highlighted to the LineChart and RadialHeatmap via the highlight prop. To do this, we use Reflux’s ListenerMixin to allow LinkedHighlightApp to listen to the ChartStore. I know that Reflux provides some more convenient mixins like connect, but I prefer the explicitness of having this.listenTo in componentDidMount.
Now both LineChart and RadialHeatmap are receiving the same value for the highlight prop, so we just need to configure them to use ChartActions.highlight to update the highlight value in the store when the user mouses over the components.
Hover Behaviour on the Radial Heatmap
The radial heatmap is fairly simple to handle because each data point corresponds to a circle element in the DOM. This means that we can attach mouse listeners using React as we would with any other component to get the behaviour we want. We create a simple callback called _handleHover that takes the hovered on data point as an argument and uses it to call ChartActions.highlight with.
UPDATE 7/31/15: It’s better to use mouseenter and mouseleave events to prevent flickering when the highlight is set to null. React seems to only re-render once after both events have fired as opposed to with mouseover and mouseout.
This gets us the mouse behaviour that we want, but we’ll need to style the highlighted circle differently to make it stand out. To do so, we’ll add the highlight class to the circle whose radius value matches the highlighted point’s radius’ value.
Hover Behaviour on the Line Chart
Getting mouse hover behaviour to work on the line chart is a bit more challenging since we have a single DOM element (the path) that represents all of the data points. To figure out which data point is being hovered on, we’ll use some of D3's utility functions to convert the mouse coordinates into domain coordinates and then find the point that closest matches them.
Instead of using React’s event handling system, I decided to use D3's so I could take advantage of the handy d3.mouse function, which converts the mouse’s position to coordinates relative to the SVG element. I imagine you can make this work with using React’s event handling system some way, but it wasn’t immediately obvious to me how to do it. Using the d3.mouse function allows us to use our D3 scales’ invert functions to get the domain coordinates of the mouse position. To attach the D3 mouse listeners to mousemove and mouseleave, use the componentDidMount and componentDidUnmount functions.
Once we have the domain coordinates, I use a modified version of d3.bisect to find the nearest point in the dataset. One of the problems I had with d3.bisect is that it always returns the closest element to the left of the specified point, or the closest element to the right depending on which function you call. I want it to give me whichever element is closest to the mouse, which is what my function findClosest returns. It’s not the prettiest function, but it is simple and it works.
Now we just need to add in the code to draw a small circle on the line when the highlight prop is set. We can do this by adding a circle to the DOM after the path element, causing the circle to be rendered “on top” of the path.
At this point, we have linked highlighting between the two charts! Hooray!
So, what’s actually happening?
When the user mouses over the line chart, mousemove events are fired and our callbacks figure out the nearest point to the mouse. That point is passed as an argument to ChartActions.highlight, which ChartStore is listening for. When the action fires, ChartStore’s callback onHighlight is triggered, which in turn triggers the callback in LinkedHighlightApp named _onChartStoreChange. That callback updates the state of LinkedHighlightApp, setting chartHighlight to the new highlight point. This causes LinkedHighlightApp to re-render, passing the new chartHighlight value as the highlight prop to LineChart and RadialHeatmap. When they re-render, the corresponding highlight marks are drawn on screen. Since we’re using React to handle all the DOM manipulation instead of D3, we get the benefits of React’s virtual DOM diffing for fast renders. A similar chain of events happens when mousing over the circles in the RadialHeatmap component.
And that’s all there is to it!
Where do we go from here?
A clear next step is to make the highlight marks more sophisticated. For instance, we could use an SVG text element to show the value of the highlighted points somewhere on the component, or a line element as I did on Buckets when hovering on the line charts. To do so is as simple as updating our render function to include the additional elements we want to draw when highlight is not null.
Improvements can also be made to the mouseover and mouseout behaviour in the RadialHeatmap, since they both get called when moving to adjacent circles, which causes a flicker when highlight is briefly set to null due to the mouseout callback. This can be fixed with a variation of debounced function behaviour.
UPDATE 7/31/15: Not sure why I didn’t think of this at first, but using mouseenter and mouseleave events instead takes care of this problem.
If performance becomes an issue when using the charts on pages with a number of other components, you can consider adding the PureRenderMixin to them to prevent unnecessary updates from taking place when the data or hover does not happen.
A final issue interesting to think about is handling linked highlighting when two charts are using different, but related datasets. In the case shown here, we had the same data objects in both components, so the highlight action and interpretation was very simple, but in other cases, you may need to do some interpretation of the highlight payload to determine how to use it. But that’s a topic for another post.
A demo of the end result can be seen here:
The code can be found here: