No Time Dad

A blog about web development written by a busy dad

Creating Three Circles with React and D3

Out of all of the libraries I’ve used in my development career, D3 has been one of the more intimidating. Lately, I’ve been thinking about why that might be. I don’t think I lack the JavaScript skills for D3, and I can handle the html and css to render the visualizations.

I think the intimidation comes from the math side of D3. I like to think that I have a strong background in math, but some of the visualizations I’ve looked at use math that I haven’t used or thought about in a long time. My day job doesn’t require much math per say, so I don’t think about math that I’ve learned in the past on a regular basis.

Another side of the D3 that is scary to me is the code quality. I try my best to write coherent and reusuable code as much as possible for my projects and for work. Looking over some of the fancier D3 examples I see sometimes just look like spaghetti to me. I have a hard time following the logic and often can’t make heads or tails of what the code is doing.

I guess when it comes down to it, I wonder if the lack of code quality is a symptom of of people who aren’t software engineers writing the code, and instead there are more people with a math background writing it. Hard to say. I’m still very interested in learning more about D3, though. At the same time, I want to see if I can approach it in a way that doesn’t land me in a big plate of spaghetti code.

Three circles

The creator of D3, Mike Bostock, wrote a basic tutorial on D3 called Thee Little Circles that takes you through creating a modifying three circle elements using D3. I thought this would be a perfect place for me to start with React and D3.

Why React? Well, this blog is written using Gatsby, which uses React under the hood. Also, I like React. It’s easy to use, flexible, and good for learning new concepts since it allows me to iterate quickly.

I want to separate the component logic from the D3 logic. I’ll be using React’s component lifecycle methods to interact with D3. My overarching goal is write code that’s reusable and sane. There are more than a few ways you can incorporate D3 and React, some more difficult and annoying than others. The main thing to keep in mind is that both React and D3 want to be in control of the DOM, so it’s up to you to decide who gets control.

I’ll be using React hooks in my example. I’ll also be using D3 modules instead of installing the entire D3 ecosystem. The reason for this is to try to keep the bunble size down. D3 is a huge library, and I don’t need every module in it.

All I’m going to be doing for this example is starting with an empty div element and using D3 to append an svg element and three circle elements to it. The final html should look something like this:

<div>
  <svg>
    <circle cy="75" cx="50" r="30" fill="red"></circle>
    <circle cy="75" cx="150" r="30" fill="blue"></circle>
    <circle cy="75" cx="250" r="30" fill="green"></circle>
  </svg>
</div>

Creating the circles

The first thing I want to do is create a new file to hold the D3 logic. I’ll call this file d3-circle-helpers.js. This file will contain functions related to creating the svg element, and appending the circle elements based on data passed from the React component state.

The basic idea is that the InitCircles function will initialize the parent svg element that will contain circles and call the CreateCircles function that will add the circle elements based on the data. The basic structure of the file will look like the below.

// d3-circle-helpers.js
import { select } from 'd3-selection';

export const InitCircles = (elementRef, data) => {
  ...
}

const CreateCircles = (elementRef, data) => {
  ...
}

The InitCircles function will use D3’s select method to get the specified node element from the DOM using React’s useRef hook that I’ll define later in the component. Once the ref is selected, I’ll tell D3 to append the parent svg element. After the svg element is appended, I’ll call the CreateCircle function, which will add the circle elements. So far, this will create an html structure that looks as follows:

<div>
  <svg></svg>
</div>
// d3-circle-helpers.js
import { select } from 'd3-selection';

export const InitCircles = (elementRef, data) => {
  // Using the reference from React, add an svg element
  select(ref.current).append('svg');

  // Create and add the circles to the svg element
  CreateCircles(elementRef, data);
}

const CreateCircles = (elementRef, data) => {
  ...
}

Looking good so far, but I need to add the circles. I can now use D3’s select again with the ref to select the svg element I just created. I can then bind the to the data via D3’s enter method and append the circle elements based on the data.

Binding to the data via enter tripped me up for a while. I really wanted to write a for-loop and iterate the data to create the circles. But, D3 has already created the loop when I bind to the data and use enter. I spent more time than I’d like to admit wrapping my head around this.

Once I’ve bound to the data and created the circle elements, I need to add the attributes to them. Specifically, postional attributes cx & cy, as well as the radius attribue r, and the fill attribute to set the color. The value of the fill attribute will come from the data passed the function.

// d3-circle-helpers.js
import { select } from 'd3-selection';

export const InitCircles = (elementRef, data) => {
  select(ref.current).append('svg');
  CreateCircles(elementRef, data);
}

const CreateCircles = (elementRef, data) => {
  // Using the ref, select the svg element
  const svg = select(elementRef.current).selectAll('svg');

  // Select all existing circles and bind the data
  const circles = svg.selectAll('circles').data(data);

  circles.enter()
    // Add the circles
    .append('circle')
    // Set the vertical position
    .attr('cy', 75)
    // Set the horizontal position, with extra space
    .attr('cx', (_, i) => i * 100 + 50)
    // Set the circle radius
    .attr('r', '30')
    // Use the callback to access the data (color) and set the fill value
    .attr('fill', (color) => color);
  
  // Try to clean up existing data
  circles.exit().remove();
}

Now that my D3 helper methods are done, I can turn my attention to the component itself. As mentioned previously, I’ll use React’s useEffect hook to call the InitCircles function from my d3-circle-helpers.js file. I’ll also use React’s useRef hook to create a ref object for D3 to use.

import React, { useEffect, useRef, useState } from 'react';
import { InitCircles } from './d3-circle-helpers.js';

export const MainContainer = () => {
  // Define some colors to pass to D3
  const [data] = useState(['red', 'blue', 'green']);

  // Initialize a ref for the parent container div
  const containerRef = useRef(null);

  // Lifecycle hook where the circles will be initialized
  useEffect(() => {
    InitCircles(containerRef, data);
  }, [data]);

  return (
    // Create the parent element and reference
    <div ref={containerRef}></div>
  );
}

And what results from that is three circles; one red, one red, and one green. Not exactly the most exhilarating data visualization, but I think this is a strong foundation to build on. The main thing for me was to keep the D3 code out of the React code as much as possible. I think this is the first step towards keeping the code managable and easy to understand. It was also a very simple use of D3, so I’ll see how it goes when I try to create something more involved.

Final code & demo

// MainContainer.jsx
import React, { useEffect, useRef, useState } from 'react';
import { InitCircles } from './d3-circle-helpers.js';

export const MainContainer = () => {
  const [data] = useState(['red', 'blue', 'green']);
  const containerRef = useRef(null);

  useEffect(() => {
    InitCircles(containerRef, data);
  }, [data]);

  return (
    <div ref={containerRef}></div>
  );
}
// d3-circle-helpers.js
import { select } from 'd3-selection';

export const InitCircles = (elementRef, data) => {
  select(ref.current).append('svg');
  CreateCircles(elementRef, data);
}

const CreateCircles = (elementRef, data) => {
  const svg = select(elementRef.current).selectAll('svg');
  const circles = svg.selectAll('circles').data(data);

  circles.enter()
    .append('circle')
    .attr('cy', 75)
    .attr('cx', (_, i) => i * 100 + 50)
    .attr('r', '30')
    .attr('fill', (color) => color);
  
  circles.exit().remove();
}
// package.json
...
"react": "^16.12.0",
"d3-selection": "^2.0.0",
...