4 min read 0 comments

As someone who got introduced to coding via an interest in data science, I have inevitably encountered D3js well before I learned how to code in JavaScript. Looking back, I found it hard to adapt it in normal data visualization projects as I didn’t really fully understood what each of the methods used do. However, I still came up with a small project using D3js, while not knowing much what was going on under the hood. Now that I know JavaScript and also React, I got excited about going back to D3js and how I can use it with React.

First, I went back to how I learned D3js. Back then, I came across a video tutorial by Scott Murray on O’Reilly that I found very useful for someone who didn’t have a background in JavaScript. This time, I used the second edition of his book, “Interactive Data Visualization for the Web: An Introduction to Designing with D3”. I found it now easier to understand, and have attempted to render the D3js codes in React.

Of course, I looked for what others have been doing to make this possible, and found numerous blogs that talk about it. There are various ways, and I still have a long way to go in my research to figure out the best way to do this. My main take away from my preliminary scouring of “literature” on D3js and React is that while D3js manipulates the DOM, React manipulates the virtual DOM. I also found that various libraries now exist for using both React and D3js. Some of these include:

  • React-vis, created by Uber
  • React D3, a fairly established library that provides React components that render basic plots and an array of plot functionalities

More are listed in the article written in Smashing Magazine by Marcos Iglesias.

Also, I got the impression that D3 and React together is not well established at this point. But just to get me started, I went on to try a few codes to render the examples from Murray’s book. One blog that helped me was that by John Tucker (“If and When to Use D3.js with React”). Here, incorporating D3js in React involves taking advantage of a few lifecycle hooks in React, including componentDidMount, componentWillReceiveProps, and shouldComponentUpdate. Using these lifecycles actually allowed me to render a scatter plot:

To get me there, I had to do some crazy things like importing the default D3 bundle as a namespace, just to make it easier for me to freely use the codes from Murray’s book without importing individual functions in D3. This particular example illustrated the construction of a scatterplot from a randomly generated dataset. The following is the code for the React component that renders the plot using a simple Node.js-Express setup as a starting point for my page.

import React from "react";
import * as d3 from "d3";
import { generateDataset } from "./generateDataset"; // separate file for generating the datapoints for the scatterplot, shown at the end of this blog

class Plot extends React.Component {
  constructor() {
    super();
    this.plot = this.plot.bind(this);
  }

  componentDidMount() {
    const { dataset } = generateDataset();
    this.plot(dataset);
  }

  componentWillReceiveProps({ dataset }) {
    this.plot(dataset);
  }

  shouldComponentUpdate() {
    return false;
  }

  plot() {
    // code directly copied from Murray's book (but using ES6)
    const h = 300,
      w = 500,
      padding = 30;
    const xScale = d3
      .scaleLinear()
      .domain([0, d3.max(dataset, d => d[0])])
      .range([padding, w - padding * 2]);
    const yScale = d3
      .scaleLinear()
      .domain([0, d3.max(dataset, d => d[1])])
      .range([h - padding, padding]);
    const aScale = d3
      .scaleSqrt()
      .domain([0, d3.max(dataset, d => d[1])])
      .range([0, 10]);
    const xAxis = d3.axisBottom(xScale);
    const yAxis = d3
      .axisLeft()
      .scale(yScale)
      .ticks(5);

    let svg = d3
      .select("#chart")
      .append("svg")
      .attr("width", w)
      .attr("height", h);

    svg
      .selectAll("circle")
      .data(dataset)
      .enter()
      .append("circle")
      .attr("cx", d => xScale(d[0]))
      .attr("cy", d => yScale(d[1]))
      .attr("r", d => aScale(d[1]))
      .attr("fill", d => `rgb(255, 125, ${d[1]})`)
      .attr("stroke", "black");

    svg
      .append("g")
      .attr("class", "axis")
      .attr("transform", "translate(0," + (h - padding) + ")")
      .call(xAxis);

    svg
      .append("g")
      .attr("class", "axis")
      .attr("transform", "translate(" + padding + ",0)")
      .call(yAxis);
  }

  render() {
    return <div id="chart" />;
  }
}

While this made my day, I found that using a lot of the component lifecycles is a drag. It also didn’t really perfectly render my plot (I get an extra x and y axes without the data points). I thought that maybe there is a simpler way to do it. Can’t I just use componentDidMount alone? My laziness (and persistence, and probably, ignorance) actually paid off when I was able to do that.

I only became successful when I used async-await in componentDidMount and adding a plugin in my Babel configuration to avoid a reference error, regeneratorRuntime is not defined.

// .babelrc
{
  "presets": ["react", "env", "stage-2"],
  "plugins": [
    [
      "transform-runtime",
      {
        "polyfill": false,
        "regenerator": true
      }
    ]
  ]
}

This way, I can write my component like so:

// same lines for importing modules as above

class Plot extends React.Component {
  constructor() {
    super();
    this.state = {
      dataset: []
    };
    this.plot = this.plot.bind(this);
  }

  async componentDidMount() {
    const dataset = await generateDataset();
    this.setState({
      dataset
    });
    this.plot(this.state.dataset);
  }

  plot(dataset) {
    // same code as above
  }

  render() {
    return <div id="chart" />;
  }
}

Writing the component this way may not be suitable for certain cases as using setState() causes extra rendering. But, to me, it allows for an easier way to manipulate data via the state of the component while allowing D3js do the DOM manipulations for the plot.

Other References:

How to use D3 with React and Redux

Index:

Here is the code for generating the dataset, also from Murray’s book.

// generateDataset.js
export const generateDataset = () => {
  const dataset = [];
  const numDataPoints = 50;
  let xRange = Math.random() * 1000,
    yRange = Math.random() * 1000;
  for (let i = 0; i < numDataPoints; i++) {
    let newNumber1 = Math.floor(Math.random() * xRange);
    let newNumber2 = Math.floor(Math.random() * yRange);
    dataset.push([newNumber1, newNumber2]);
  }
  return dataset;
};