React for Data Visualization
Student Login

Quickly integrate any D3 code in your React project with Blackbox Components

This section teaches you the mental models of blackbox components and builds them up through class-based components. If you don't care about the details, you can jump ahead to using them with React Hooks.

Blackbox components are the quickest way to integrate D3 and React. You can think of them as wrappers around D3 visualizations.

With the blackbox approach, you can take any D3 example from the internets or your brain, wrap it in a React component, and it Just Worksโ„ข. This is great when you're in a hurry, but comes with a big caveat: You're letting D3 control some of the DOM.

D3 controlling the DOM is okay, but it means React can't help you there. That's why it's called a Blackbox โ€“ย React can't see inside.

No render engine, no tree diffing, no dev tools to inspect what's going. Just a blob of DOM elements.

Okay for small components or when you're prototyping, but I've had people come to my workshops and say "We built our whole app with the blackbox approach. It takes a few seconds to re-render when you click something. Please help"

๐Ÿค”

Here's how it works:

  • React renders an anchor element
  • D3 hijacks it and puts stuff in

You manually re-render on props and state changes. Throwing away and rebuilding the entire DOM subtree on each render. With complex visualizations this becomes a huge hit on performance.

Use this technique sparingly.

A quick blackbox example - a D3 axis

Let's build an axis component. Axes are the perfect use-case for blackbox components. D3 comes with an axis generator bundled inside, and they're difficult to build from scratch.

They don't look difficult, but there are many tiny details you have to get just right.

D3's axis generator takes a scale and some configuration to render an axis for us. The code looks like this:

const scale = d3.scaleLinear().domain([0, 10]).range([0, 200])
const axis = d3.axisBottom(scale)
d3.select("svg").append("g").attr("transform", "translate(10, 30)").call(axis)

You can try it out on CodeSandbox.

If this code doesn't make any sense, don't worry. There's a bunch of D3 to learn, and I'll help you out. If it's obvious, you're a pro! This book will be much quicker to read.

We start with a linear scale that has a domain [0, 10] and a range [0, 200]. Scales are like mathematical functions that map a domain to a range. In this case, calling scale(0) returns 0, scale(5) returns 100, scale(10) returns 200. Just like a linear function from math class โ€“ย y = kx + n.

We create an axis generator with axisBottom, which takes a scale and creates a bottom oriented axis โ€“ numbers below the line. You can also change settings for the number of ticks, their sizing, spacing, and so on.

Equipped with an axis generator, we select the svg element, append a grouping element, use a transform attribute to move it 10px to the right and 30px down, and invoke the generator with .call().

It creates a small axis:

Simple axis

Play around with it on Codesandbox. Change the scale type, play with axis orientation. Use .ticks on the axis to change how many show up. Have some fun ๐Ÿ˜ƒ

A quick blackbox example - a React+D3 axis

Now let's say we want to use that same axis code but as a React component. The simplest way is to use a blackbox component approach like this:

class Axis extends Component {
gRef = React.createRef()
componentDidMount() {
this.d3render()
}
componentDidUpdate() {
this.d3render()
}
d3render() {
const scale = d3.scaleLinear().domain([0, 10]).range([0, 200])
const axis = d3.axisBottom(scale)
d3.select(this.gRef).call(axis)
}
render() {
return <g transform="translate(10, 30)" ref={this.gRef} />
}
}

So much code! Worth it for the other benefits of using React in your dataviz. You'll see ๐Ÿ˜ƒ

We created an Axis component that extends React's base Component class. We can't use functional components because we need lifecycle hooks.

Our component has a render method. It returns a grouping element (g) moved 10px to the right and 30px down using the transform attribute. Same as before.

A React ref saved in this.gRef and passed into our <g> element with ref lets us talk to the DOM node directly. We need this to hand over rendering control to D3.

The d3render method looks familiar. It's the same code we used in the vanilla D3 example. Scale, axis, select, call. Only difference is that instead of selecting svg and appending a g element, we select the g element rendered by React and use that.

We use componentDidUpdate and componentDidMount to keep our render up to date. Ensures that our axis re-renders every time React's engine decides to render our component.

That wasn't so bad, was it?

Try it out on Codesandbox.

You can make the axis more useful by getting positioning, scale, and orientation from props. We'll do that in our big project.

Practical exercise

Try implementing those as an exercise. Make the axis more reusable with some carefully placed props.

Here's my solution, if you get stuck ๐Ÿ‘‰ https://codesandbox.io/s/5ywlj6jn4l

A D3 blackbox higher order component โ€“ HOC

After that example you might think this is hella tedious to implement every time. You'd be right!

Good thing you can abstract it all away with a higher order component โ€“ย a HOC. Now this is something I should open source (just do it already), but I want to show you how it works so you can learn about the HOC pattern.

Higher order components are great when you see multiple React components sharing similar code. In our case, that shared code is:

  • rendering an anchor element
  • calling D3's render on updates

With a HOC, we can abstract that away into a sort of object factory. It's an old concept making a comeback now that JavaScript has classes.

Think of our HOC as a function that takes some params and creates a class โ€“ a React component. Another way to think about HOCs is that they're React components wrapping other React components and a function that makes it easy.

A HOC for D3 blackbox integration, called D3blackbox, looks like like this:

function D3blackbox(D3render) {
return class Blackbox extends React.Component {
anchor = React.createRef()
componentDidMount() {
D3render.call(this)
}
componentDidUpdate() {
D3render.call(this)
}
render() {
const { x, y } = this.props
return <g transform={`translate(${x}, ${y})`} ref={this.anchor} />
}
}
}

You'll recognize most of that code from earlier.

We have componentDidMount andcomponentDidUpdate lifecycle hooks that call D3render on component updates. render renders a grouping element as an anchor with a ref so D3 can use it to render stuff into.

Because D3render is no longer a part of our component, we have to use .call to give it the scope we want: this class, or rather this instance of the Blackbox class.

We've also made some changes that make render more flexible. Instead of hardcoding the translate() transformation, we take x and y props. { x, y } = this.props takes x and y out of this.props using object decomposition, and we used ES6 string templates for the transform attribute.

Consult the ES6 cheatsheet for details on the syntax.

Using our new D3blackbox HOC to make an axis looks like this:

const Axis = D3blackbox(function () {
const scale = d3.scaleLinear().domain([0, 10]).range([0, 200])
const axis = d3.axisBottom(scale)
d3.select(this.anchor).call(axis)
})

You know this code! We copy pasted our axis rendering code from before, wrapped it in a function, and passed it into D3blackbox. Now it's a React component.

Play with this example on Codesandbox, here.

Previous:
The building blocks
Next:
D3blackbox magic trick - render anything in 30sec (5:11)
Created by Swizec with โค๏ธ