D3 & React
D3 is a great tool to build out interactive data visualizations and React has become one the defacto front end frameworks. Together they are quite powerful however they both want to control the DOM. They both take control of user interface elements and do so in different ways.
#
The React & D3 EcosystemThere are so many React charting libraries out today that are built on D3. To name a few there there:
Here is a small CodeSandbox demo of a few of those libraries.
https://codesandbox.io/s/late-tree-ix7eh?file=/src/App.js
#
What We Are BuildingFor our demo we will be taking some D3 code and refactor it to work within a React Component. This will involve importing and using:
- useState
- useEffect
- useRect
Here is the Alphabet Solution we will aim to complete.
https://codesandbox.io/s/alphabet-d3react-d3-within-react-95ueg?file=/src/Components/Letters.js
#
ApproachesIf you have worked with React then you know that it takes on the responsibility to update all DOM elements and takes ownership in responding to all events. DOM elements are updated when there is a change in state.
If you have worked with D3 then you know that it takes on the same responsibility as React and updates DOM elements based on it's own enter-update-exit methodology
So how do we coerce these two powerful tools to work together?
There are several approaches we can take:
- D3 within React
- React for the DOM, D3 for Math
- React/D3 Libraries
#
D3 Within ReactThe first approach is to give our D3 code as much DOM control as possible. It uses a React component to render an empty SVG element that React then provides D3 access to as a reference or ref
It then uses lifecycle methods to create the and update the chart.
#
Getting StartedWe will be using the following starter code:
https://codesandbox.io/s/alphabet-d3react-d3-within-react-starter-v69le
We will also import some of the code from the previous D3 only build:
https://codesandbox.io/s/alphabet-d3react-d3-within-react-solution-fxluj
#
App.jsApp.js has been updated to render a Letter
Component.
const App = () => { return ( <div className="App"> <> <Letters /> </> </div> );}
export default App;
#
Letter.jsA Letter
Component has been created to give us a head start some of the previous code has been ported over such as:
alphabet
arrayrender
function.start
&shuffle
functions
#
Adding The D3.js DependencySince we in React we need to add d3.js as a dependency and the import
it into the Letter
Component.
#
Importing D3With the d3.js library installed we must now import it. D3.js comes with many submodules that we could import specifically, much like we do when importing {useState, useEffect}
but for the sake of simplicity we will import the entire library.
import * as d3 from "d3";
We will need to make a few edits to the render
method as well. Since the svg and g are already in place there is no need to create them so we only need to select them.
const letters = d3 .select('svg') .select('g') .selectAll('text') .data(dataSet, (d) => d);
The Component will also render the html from the previous example however we've gone ahead and added the svg and g elements so React can render them right from the start.
You will also notice that both the Start button has been refactored to use React's onClick
event.
return ( <> <div class="container"> <div id="viz"> <svg> <g transform="translate(50 100)"></g> </svg> </div> <button id="start" type="button" onClick={start}> Start </button> <button id="stop" type="button"> Pause </button> </div> </>);
At this point we should see the following rendered on the page:
#
React SetupAlthough we are allowing D3 to control the DOM we will still leverage React and it's ability to re-render the Component when state has been updated.
So let's import the following Hooks to get us started, one of which you might not have yet been exposed to:
- useState
- useEffect
- useRef
import React, { useState, useEffect, useRef } from "react";
#
useStateLet's setup up state to use the alphabet array.
const [dataSet, setDataSet] = useState(alphabet);
#
useEffectThe first useEffect that we will need will be used to call the render
method on componentDidMount lifecycle, however we also need this effect to run on componentDidUpdate lifecycle as well so we will add dataSet
as a dependency.
useEffect(() => { render(dataSet);}, [dataSet]);
If we refresh the page we should see the letters render for the first time.
Also try starting and pausing the transitions as they should work as well.
#
Refactor For ReactAlthough we are allowing D3 to to update the DOM we still want to refactor our code a bit to respect the React environment. The first refactor is to reference
the svg instead of grabbing it directly.
Let's create the ref.
const ref = useRef();
Assign the ref
<svg ref={ref}> <g transform="translate(50 100)"></g></svg>
Update D3 to se the svg reference.
const letters = d3 .select(ref.current) .select("g") .selectAll("text") .data(dataSet, (d) => d);
#
React To Control PauseIf we look at the start
function we can see that D3 is still controlling clearing the interval. clearInterval has been added to the start function in order to provide access to the sI variable and make it available within it's scope.
const start = () => { const sI = setInterval(() => { shuffle(); }, 3000); d3.select("#stop").on("click", () => clearInterval(sI));};
We now are going to replace all that code with a useEffect that uses a dependency. Let's create the dependency first.
const [timer, setTimer] = useState(false);
Now let's use useEffect in a way that you haven't yet seen. As you know useEffect runs whatever code you provide and can rerun depending on wether it's dependency has changed. What might be new here is that it also can return a callback. The callback here is used to clean up whatever had been created by the component but is no longer needed, hence the setInterval
useEffect(() => { if (timer) { const sI = setInterval(() => { shuffle(); }, 3000); } return () => clearInterval(sI);}, [timer]);
Now let's update the start
method.
const start = () => { const newTimer = !timer; setTimer(newTimer);};
And of course update the Pause button so that it now calls the start function
<button id="stop" type="button" onClick={start}>Pause</button>
And there you have it.