Level Up Coding

Coding tutorials and news. The developer homepage gitconnected.com && skilled.dev && levelup.dev

Follow publication

React & D3: Rendering A Map

--

Ever since I discovered D3 I was immediately fascinated with all its capabilities. It was built as a low level JavaScript data visualization library which meant you were only limited by your imagination as to what you could build, render, and interact with on the screen. It’s also one of the most widely used JavaScript data viz libraries with over 95k stars on Github.

I studied it heavily for several years after graduating the WDI (Web Development Immersive) program at GA back in 2015 and even ran a weekly meetup at their NYC campus back in 2018.

In 2016 I was hired by GA to teach my very first WDI program and needed to learn React pronto as the team I was working with decided to incorporate it into our curriculum. The more I learned about React, the more intrigued I was and because the demand to master all the content, D3 was put on hold.

That was until the last few months when I decided to finally jump back into D3 and decided to refactor a previous D3 dashboard project I built called Streetball Mecca.

Much of this refactoring involved replacing D3 to create and/or update/delete elements and hand that over to React. One of Reacts strengths is managing state so initially chose useState but then refactored it over to useReducer as it is a much better choice for managing state and complex business logic. Lastly I broke the design into smaller and smaller components for manageability.

Integrating D3 and React

Integrating React and D3 requires making the decision on how much control to give, or take away from, D3 as both D3 and React want full control over the DOM.

Here are the 3 basic approaches you should consider:

  • D3 within React — React renders an svg and D3 does all the rest
  • D3 for it’s unique methods (scales, axes, projections, etc..) and React to manage state and update the DOM
  • Using a 3rd party React/D3 library such as high level one like Nivo or a low level like VX.

Each approach has their advantages/disadvantages and in the end it comes down to which technology you are most proficient, D3 or React.

D3 within React

At first, I took the D3 within React approach as it was the easiest way to migrate the existing codebase. At this point I was well versed in both libraries but combining the two was a new adventure and required a bit of research and many trials and tribulations.

After I reviewed the original design I decided to limit D3 to just the map, circles and bar chart (including the x axis) and allow React to render all other elements.

The D3 Map Component

For the Map, I mostly copied over the renderChart function and created a ref(erence) to the svg. Ref’s in React are just a reference which can be to a DOM node or JavaScript value. In the use case of the svg I equate it to using document.querySelector grab a DOM element. On the other hand assigning a ref to a JS value is done so to maintain that value through one or more state changes.

The ref is an object that stores the value it’s assigned to its current key which would then be passed to the d3.select() in order to grab that DOM element.

Here the ref is instantiated using the useRef hook and then the svgRef is assigned to the svg element using ref={svgRef}.

import React from 'react'
import * as d3 from 'd3'
const Map = (props) => { //////////////////////////////////////////////////////
// REFs
//////////////////////////////////////////////////////
const svgRef = useRef(); //////////////////////////////////////////////////////
// RENDER THE CHART USING D3
//////////////////////////////////////////////////////
const renderChart = (nyc, path) => {
d3.select(svgRef.current)
//...additional d3 code...
};
//////////////////////////////////////////////////////
// RENDER THE SVG
//////////////////////////////////////////////////////
return(
<svg id="boroughs-map" ref={svgRef}></svg>
)
}

Another ref was then instantiated for the D3 projection, in this case d3.geoMercator(). In this instance it’s being used as instance variable within the functional component as this value will be referenced by both the map and the circles at different times during the components lifecycle.

//////////////////////////////////////////////////////
// REFs
//////////////////////////////////////////////////////
const svgRef = useRef()
const projRef = useRef(d3.geoMercator()
.center([-73.93, 40.72]).scale(57500));

useEffect is then used to grab the height/width of the svg which is used to center the projection. useEffect is being implemented here as a componentDidUpdate lifecycle method that has a single dependency. Any change in value to the dependency will cause the useEffect method to rerun.

//////////////////////////////////////////////////////
// USEEFFECT AS COMPONENTDIDUPDATE WITH DEPENDENCY
//////////////////////////////////////////////////////
useEffect(() => {
const height = svgRef.current.clientHeight
const width = svgRef.current.clientWidth
projRef.current.translate([width / 2, height / 2 ]);}, [data])

The projection is D3 specific and it defines how the map will be displayed. D3 comes with various projections with .geoMercator() being the most commonly used. In this particular use case it needs to be centered using translate which requires the height/width values.

//////////////////////////////////////////////////////
// USEEFFECT AS COMPONENTDIDUPDATE WITH DEPENDENCY
//////////////////////////////////////////////////////
useEffect(() => {
const height = svgRef.current.clientHeight
const width = svgRef.current.clientWidth
projRef.current.translate([width / 2, height / 2 ]);}, [data])

The projection is then passed to d3.geoPath() to draw the actual svg paths that define the shapes for the boroughs in the map. I’ve also included an if statement that will pass the map data and path to renderChart only when there is actual data to render.

//////////////////////////////////////////////////////
// USEEFFECT AS COMPONENTDIDUPDATE WITH DEPENDENCY
//////////////////////////////////////////////////////
useEffect(() => {
const height = svgRef.current.clientHeight
const width = svgRef.current.clientWidth
projRef.current.translate([width / 2, height / 2 ]); const path = d3.geoPath().projection(projeRef.current) if (data.length) {
renderChart(data[0], path)
}
}, [data])

Here is all the code thus far.

//////////////////////////////////////////////////////
// REFs
//////////////////////////////////////////////////////
const svgRef = useRef()
const projRef = useRef(d3.geoMercator()
.center([-73.93, 40.72]).scale(57500));
//////////////////////////////////////////////////////
// USEEFFECT AS COMPONENTDIDUPDATE WITH DEPENDENCY
//////////////////////////////////////////////////////
useEffect(() => {
const height = svgRef.current.clientHeight
const width = svgRef.current.clientWidth
projRef.current.translate([width / 2, height / 2 ]); const path = d3.geoPath().projection(projeRef.current) if (data.length) {
renderChart(data[0].features, path)
}
}, [data])

As for the renderChart function, it’s only 8 lines of actual code and is all D3:

  • data binding — .data(data)
  • data entering — .enter()
  • appending data — .append(‘path’)
  • appending attributes — .attr(‘d’, path)
  • appending styles — .style(‘fill’, (d) => boroughLegend(d.properties.borough))
const renderChart = (data, path) => {
d3.select(svgRef.current).selectAll('path')
.data(data).enter().append('path')
.attr('class', (d) => d.properties.name)
.attr('d', path)
.style(‘fill’, (d) => boroughLegend(d.properties.borough))
};

D3 for the math and React for the DOM

Although this worked to render the map I knew that some portion of the D3 code could be supplanted with React. So the question was what elements could be easily rendered by React and the answer was quite clear, the path elements.

So the following refactoring would be required:

  • create a new ref to store the value of d3.geoPath() — this was because I decided to move the conditional logic from useEffect to the return statement
  • update useEffect to update the new pro
  • use .map() to iterate over the data and create the path elements and assign them the needed properties
  • use conditional logic inside in the components return statement to render the paths if there was data

Adding the new ref required little effort.

//////////////////////////////////////////////////////
// REFs
//////////////////////////////////////////////////////
const svgRef = useRef()
const projRef = useRef(d3.geoMercator()
.center([-73.93, 40.72]).scale(57500));
const pathRef = useRef()

Updating the pathRef in useEffect also was no big deal. I also cleaned up useEffect a bit by removing the conditional logic.

//////////////////////////////////////////////////////
// USEEFFECT AS COMPONENTDIDUPDATE WITH DEPENDENCY
//////////////////////////////////////////////////////
useEffect(() => {
const height = svgRef.current.clientHeight
const width = svgRef.current.clientWidth
projRef.current.translate([width / 2, height / 2 ]); pathRef.current = d3.geoPath().projection(projRef.current);}, [data])

renderChart() required the most effort but I wouldn’t even go that far. All it did was replace D3’s data binding and enter methods with mapping over the array, creating the paths and assigning the the properties.

const renderChart = () => {
return data[0].features.map((d,i) => {
const featurePath = `${pathRef.current(d)}`
return (
<path
key={i}
d={featurePath}
className={d.properties.name}
fill={boroughLegend(d.properties.borough)}
/>
)
})
};

And the last step was to add the conditional logic to the Map components return statement.

return (
<svg id="boroughs-map" ref={svgRef}>
{data.length && renderChart()}
</svg>
);

Conclusion

I have to say that I prefer this approach over using D3 to render the map. I suppose it’s because I’ve been working so heavily with React these days that I believe it should manage the DOM whenever possible, which is almost always.

In a way, I’m almost sad to supplant D3 with React for this functionality, as I had grown accustomed to D3’s Data Binding and Enter/Update/Exit pattern and it was something I always made a point to teach in any intro to D3 class.

In truth, React seems better suited for rendering the elements and keeping track of any state changes via useState or useReducer. D3 is still powerful and efficient in its own right and offers many features not available in React so I’m glad I still get to incorporate it into this project.

Here is the CodeSandbox for the map and circles and includes 2 files with the code for rendering the map via D3 and the refactor over to React. Give them both a test run and let me know if you see any additional room for improvement.

I’ve also included the input and dropdown functionality for filtering the parks based on the chosen park or selected borough.

I’m in the process of writing a followup article where I replace D3 for generating and animating the circles with a React Circle Component as well as incorporate React-Spring for animating the circles.

If you are interested in reading more about this project, check out my previous article on React: Managing Complex State Transitions with useReducer.

--

--

Written by Joe Keohan

Software Engineering Instructor at General Assembly. React and D3 evangelist and passionate about sharing all this knowledge with others.

No responses yet