Building Time-lapse Visualizations with React Hooks and D3 (part 3)

John Bellizzi
4 min readApr 2, 2020

--

In part 2 of this tutorial, we added a visualization to control the timer and display the transitioning state of data. In this final part, we’ll explore utilizing the timer output to display the current state of data onto a US map. We’ll also see how the D3 enter/update/exit methods can make visualization rendering with transitions easy.

Before we dive into the Map component, let’s take a quick look at the data that we need to incorporate into our project. Here we have a data source that breaks down per date and per US county, the number of confirmed COVID-19 cases to date. It combines the New York Times data set we referenced earlier with the geo location of each county in the form of latitude and longitude points.

We could pass the entire data object into our Map component, be we are only ever plotting 1 date at a time in the map. A cleaner implementation is to pre-filter the data first to only records for the current date output from our timer, and then pass that data into the map.

First, we take our flat data structure and nest it by date using the d3 nest function. We convert this nesting into a hash table so that we can look up the data for the date when the timer increments to a new day.

This makes it easier to take our current timer value and extract the data we need from our hash table

We also need to find the maximum # of cases across all counties and dates so that our circle sizes in the Map can be scaled appropriately. (Note: this is calculated from the flat data)

Now we can pass this all to our Map component as such

Now let’s start building our Map component. Our component takes in the data array for the specified date and the maxCases value, and returns an svg that will render a map and data circles.

Next we’ll create a scale to size the radius of of our circles representing the number of cases in a county. In this case, we use a square root scale so there is higher discrepancy across counties with smaller total confirmed cases

Now, we’ll run a side effect in a useEffect hook to render the map and overlay circles. First we need to set up the functions that will create our map. geoPath() is a path generator that uses GeoJSON data to render svg paths (like the feature collection of states we’re pulling from here). We could directly use geoPath() to render the feature collection data set as path elements in our svg, but using an Albers projection is better suited toward displaying spherical coordinates on a flat plane (for this we use geoAlbersUsa()).

With this pathGenerator, we can now plot the states on our svg element. Here we are using the array of state features and create a path for each one, generated using the pathGenerator function. We aren’t using any update or exit logic in this section as we don’t anticipate changes in the states’ paths.

Finally, we can add the circles. Because this chart is dynamic and data is constantly being passed in, we need to define the d3 render in a way that will smoothly transition the points onto the map, and increase their size as data is updated

We’re using the Enter/Update/Exit methodology from d3 to gracefully handle the changing data state coming into the component. In order for d3 to index data points so it can differentiate what is a new data point, and what data point existed in the prior state, we define a key within our data function. The callback function above uses the FIPS code field to define a unique key for each data point data(data, d => d.FIPS).

Everything after the enter() function defines what happens to a new data point when it is added to our data set, and everything following the merge() function defines what happens when a data point already exists, but its value has changed.

Let’s take an example of New York City case data. If on February 29th there are no data points for New York City, it won’t exist in the data set and our d3 function will not have seen the FIPS code in the data. If then on March 1st NYC has its first case, the function will recognize the new data point and run everything after the enter() function, appending a new circle, giving it a class, and styling it. It will also set its cx, cy, and r values.

Now we advance to March 3rd and NYC has increased its case count from 1 to 2. Since NYC already exists in the data set, the function won’t enter a new data point, but the data has updated so we will run the update part of our function, which means increasing the r value to a larger radius representing 2 cases.

Finally, we are removing any data points that exit our data set. Say the timer pauses at March 3rd, and then we select February 1st. There are no data points for NYC for this selection state, so when d3 gets its updated data set, it recognizes that all NYC data points have exited, and the corresponding circles are removed from the DOM.

One last effect we are implementing here is a transition on the radius size. Without the transition().duration(1000) function, you’ll see that the radius sizes jump from 1 state to the next. Adding a transition makes the size increase (or decrease) smoothly.

This wraps up the series on time-lapse visualizations. As in prior sections, the full code is accessible from GitHub. If you have any questions or feedback, leave a comment and I will try to respond as soon as possible. If

--

--