Skip to content

Using D3 in React

veerleprins edited this page Jan 10, 2021 · 6 revisions

There are several libraries that can be used to make graphs in javascript. For this course we used the library D3.js. D3.js is described on the website as:

A library for manipulating documents based on data. (Source)

During the course frontend data we already worked with D3 with vanilla javascript. Now the intention was to use this in combination with the chosen framework. So in my case React.

Jump to:

Install D3 within react

To load D3 via React I started by searching online for the best way to do this. I already knew that I didn't want to load D3 as a script tag and started looking for another solution. So I ended up on a site where D3 is downloaded via npm (Source). I installed D3 by typing the following into my console:

$ npm install d3

Then I was able to import certain methods of D3 within my components at the top by typing (Note: In place of the three dots there is usually the name of a specific method you want to import):

import { ... } from 'd3';

Luckily the download of d3 went without errors so I could start making the graphs.

Creating a bar chart

My first goal was to make a bar chart. But with this, I really wanted to challenge myself by doing this using components. But, first I started by creating a bar chart literally only within my App component. I only made the rectangles for the bars (so without the axes). I did this with the help of Curran Kelleher's React with D3 tutorial.

At the top of my App component I called up the parts I needed to create the bar chart. I did this by importing the scaleBand, scaleLinear and max from d3:

import { scaleBand, scaleLinear, max } from "d3";

Then I copied and pasted the xScale and yScale variables that I had already created during the frontend-data course. Of course I changed the keys since my dataset was different now:

const yScale = scaleBand()
  .domain(electric.map((d) => d.brand))
  .range([0, height]);

const xScale = scaleLinear()
  .domain([0, max(electric, (d) => d.value)])
  .range([0, width]);

This went well, but then came the hardest part. Instead of creating a group element and using method chaining, I now planned to create the elements within jsx right away. By mapping over the data, I could then create a rectangle per element that I placed in a svg.

<svg width={width} height={height}>
  {electric.map((d) => (
    <rect
      x={0}
      y={yScale(d.brand)}
      width={xScale(d.value)}
      height={yScale.bandwidth()}
    />
  ))}
</svg>

That's how I made my first bar chart! But I did have a warning in my console:

Warning: Each child in a list should have a unique "key" prop.

At the beginning I ignored this because the bar chart worked. But what I did wrong is that React asks for a key within the children of a map or foreach to keep track of which item the loop is on. I eventually solved this by simply passing a key attribute to the rect tag with a specific value in it. I solved this as I had to fix all warnings to be able to deploy my site. You can read more about this on the page: Wiki - Deploying the Application

After this I decided to add the axes to the graph. Despite the help of Curran Kelleher's tutorials, this didn't quite work out the way I wanted. In Curran's tutorial he makes a bar chart from left to right while I wanted a bar chart from bottom to top. When I tried to adjust this, I got no errors but didn't see my rectangles appear on the screen. In hindsight, this had to do with the scaleBand and scaleLinear that I forgot to swap.

When I flipped the scaleBand and scaleLinear my bar chart finally worked the way I wanted. But my code was still inside the App component at this point:

<svg width={width} height={height}>
  <g transform={`translate(${margin.left}, ${margin.top})`}>
    {xScale.ticks().map((tickValue) => (
      <g key={tickValue} transform={`translate(${xScale(tickValue)}, 0)`}>
        <line y2={innerHeight} stroke="black" />
        <text y={innerHeight + 3} dy=".71em" style={{ textAnchor: "middle" }}>
          {tickValue}
        </text>
      </g>
    ))}
    {yScale.domain().map((tickValue) => (
      <text
        key={tickValue}
        style={{ textAnchor: "end" }}
        x={-3}
        dy=".32em"
        y={yScale(tickValue) + yScale.bandwidth() / 2}
      >
        {tickValue}
      </text>
    ))}
    {electric.map((d) => (
      <rect
        key={d.brand}
        x={0}
        y={yScale(d.brand)}
        width={xScale(d.value)}
        height={yScale.bandwidth()}
      />
    ))}
  </g>
</svg>

For this reason I started splitting the code into different components. First I created three files / components for this: The axisBottom.js, axisLeft.js and the bars.js. Below you can see my code per component:

// Axis bottom component:
export const AxisBottom = ({ xScale, innerHeight }) =>
  xScale.domain().map((tickValue) => (
    <text
      key={tickValue}
      y={xScale(tickValue) + xScale.bandwidth() / 2}
      x={-innerHeight - 10}
      style={{ textAnchor: "end" }}
      transform={`rotate(-90)`}
    >
      {tickValue}
    </text>
  ));
// Axis left component:
export const AxisLeft = ({ yScale, innerWidth, tickFormat }) =>
  yScale.ticks().map((tickValue) => (
    <g key={tickValue} transform={`translate(0, ${yScale(tickValue)})`}>
      <line className="lines" x2={innerWidth} />
      <text x={-3} dy=".32em" style={{ textAnchor: "end" }}>
        {tickFormat(tickValue)}
      </text>
    </g>
  ));
// Bars component:
export const Bars = ({ data, yScale, xScale, xValue, yValue }) =>
  data.map((d) => (
    <rect
      key={yValue(d)}
      x={0}
      y={yScale(yValue(d))}
      width={xScale(xValue(d))}
      height={yScale.bandwidth()}
    />
  ));

I put these components in my folder 'atoms' and in my App component I called these three components and gave the props:

// Importing the components:
import { AxisBottom } from "./Components/Atoms/AxisBottom";
import { AxisLeft } from "./Components/Atoms/AxisLeft";
import { Bars } from "./Components/Atoms/Bars";
// The JSX piece in my App component where the three components
// are called and the props are passed:
<svg width={width} height={height}>
  <g transform={`translate(${margin.left}, ${margin.top})`}>
    <AxisBottom xScale={xScale} innerHeight={innerHeight} />
    <AxisLeft yScale={yScale} />
    <Bars
      data={electric}
      xScale={xScale}
      yScale={yScale}
      xValue={xValue}
      yValue={yValue}
    />
  </g>
</svg>

Then I created another component called barchart.js, which specifically calls all the individual components of the bar chart. Then I created another component called barChart, which specifically calls all the individual components of the barchart. Because of this I only had to call the Bar Chart component in the App component. Below is my code from the barchart.js file:

import { margin, width, height } from "../../modules/helpers/utils";
import { AxisBottom } from "../atoms/axisbottom";
import { AxisLeft } from "../atoms/axisleft";
import { Bars } from "../atoms/bars";
import { format, scaleBand, scaleLinear, max } from "d3";

// D3 BarChart with help from Curran Kelleher.
// Source: https://www.youtube.com/watch?v=y03s9MEx6mc&list=PL9yYRbwpkykuK6LSMLH3bAaPpXaDUXcLV&index=23

export const BarChart = ({ data }) => {
  const innerHeight = height - margin.top - margin.bottom;
  const innerWidth = width - margin.left - margin.right;

  const xValue = (d) => d.brand; //TESLA
  const yValue = (d) => d.value; //40000

  const xScale = scaleBand()
    .domain(data.map(xValue))
    .range([0, innerWidth])
    .padding(0.2);

  const yScale = scaleLinear()
    .domain([0, max(data, yValue)])
    .range([innerHeight, 0]);

  return (
    <svg width={width} height={height}>
      <g transform={`translate(${margin.left},${margin.top})`}>
        <g className="x-Axis">
          <AxisBottom xScale={xScale} innerHeight={innerHeight} />
        </g>
        <AxisLeft
          yScale={yScale}
          innerWidth={innerWidth}
          tickFormat={(n) => format(",d")(n).replace(",", ".")}
        />
        <text
          className="y-label"
          y={-80}
          textAnchor="middle"
          x={-innerHeight / 2}
          transform={`rotate(-90)`}
        >
          Aantal auto's
        </text>
        <Bars
          data={data}
          xScale={xScale}
          yScale={yScale}
          xValue={xValue}
          yValue={yValue}
          innerHeight={innerHeight}
        />
      </g>
    </svg>
  );
};

Because of this my Bar chart was finally ready and I could start with the other visualisations.

Creating a map

After making a bar chart with D3 in React, I already knew a bit more how I could use D3 in combination with React for other visualizations. That's how I started making a map. I was lucky that Curran Kelleher had made another video in which he explained how to make a map with D3 and React.

Before creating the map, I started by downloading topojson using npm:

$ npm install topojson

Then I created a component called Cities.js that I called from the App component and passed the data as a prop:

<svg width={width} height={height}>
  <Cities data={map} />
</svg>

In the cities component, I first imported the geoPath, geoMercator and zoom from d3:

import { geoPath, geoMercator, zoom } from "d3";

Then I made a group element in which a path was created for each municipality of the map with the correct arcs:

const projection = geoMercator().scale(4000).center([5.116667, 52.17]);
const pathGenerator = geoPath().projection(projection);

export const Cities = ({ data }) => (
  <g className="cities" cursor="zoom-in">
    {data.map((feature) => (
      <path key={feature.id} className="city" d={pathGenerator(feature)} />
    ))}
  </g>
);

Then I made the circles on the map that corresponded to my data points:

{
  garages.map((d) => {
    const [x, y] = projection([d.location.longitude, d.location.latitude]);
    return <circle className="garages" cx={x} cy={y} r={1} />;
  });
}
{
  chargingPoints.map((d) => {
    const [x1, y2] = projection([d.location.longitude, d.location.latitude]);
    return <circle className="chargingpoints" cx={x1} cy={y2} r={1} />;
  });
}

This worked, but I still thought my code was quite long and chaotic. I also noticed that I had redundancy in my code. For this reason I created a Circles.js component:

export const Circles = ({ data, keyName, projection, name }) => (
  <g className="circles">
    {data.map((d) => {
      const [x, y] = projection([d.location.longitude, d.location.latitude]);
      return <circle key={d[keyName]} className={name} cx={x} cy={y} r={2} />;
    })}
  </g>
);

I also created a component for the paths for the municipality map itself:

export const Paths = ({ data, pathGenerator }) => (
  <g className="paths">
    {data.map((feature) => (
      <path key={feature.id} className="city" d={pathGenerator(feature)}></path>
    ))}
  </g>
);

Then my Cities component (which I renamed to 'dutchmap.js') looked like this:

//Only the JSX part:
<svg
  ref={svgEl}
  width={width}
  height={height}
  >
  <g className="group" ref={svgGroup}>
    <Paths data={data} pathGenerator={pathGenerator}/>
    <Circles data={garages} keyName='areaid' projection={projection} name='garages'/>
    <Circles data={chargingPoints} keyName="areadesc" projection={projection} name="chargingpoints"/>
  </g>
</svg>

And that's how the map worked.

Creating a pie chart

Finally I wanted to make a pie chart. In the meantime, I was a bit familiar with how to create graphs in React using D3. But now the challenge was to make a pie chart, something that Curran Kelleher has not made a tutorial on.

I started by looking online for examples of how to make a pie chart. Then I started to merge the knowledge I already had (with D3 and React) with the knowledge of the online examples about making a pie chart:

import { margin, width, height, radius } from "../../modules/helpers/utils";
import { scaleOrdinal, pie, arc } from "d3";

// From example: https://www.tutorialsteacher.com/d3js/create-pie-chart-using-d3js
export const PieChart = ({ data }) => {
  const color = scaleOrdinal(["#043E1D", "#F6AE2D"]);
  const createPie = pie();
  const paths = arc().innerRadius(0).outerRadius(radius);

  return (
    <svg width={width} height={height}>
      <g transform={`translate(${margin.left},${margin.top})`}>
        {createPie(data).map((x) => (
          <g className="arc" key={x.data.keyNum}>
            <path fill={color(x.data.value)} d={paths}></path>
          </g>
        ))}
      </g>
    </svg>
  );
};

Only this didn't work the way I wanted at first. I kept getting an warning with the following message:

Warning: Invalid value for prop `d` on <path> tag. Either remove it from the element, or pass a string or number value to keep it in de DOM.

The tricky part was that I didn't understand where the warning was and the rest of my code just worked. I just didn't see the pie chart.

Finally (by searching online) I came across an example of a pie chart with D3 in React. Using this example, I saw what went wrong with my code. My 'createArc' was a function that I implemented incorrectly in my code. After tweaking this, I split my long code over several components again. For example, I created an arcPath.js component, into which I pasted the following code:

export const ArcPath = ({ data, index, createArc, colors }) => (
  <g key={index} className="arc">
    <path className="piePath" d={createArc(data)} fill={colors(index)} />
    <text
      transform={`translate(${createArc.centroid(data)})`}
      textAnchor="middle"
      alignmentBaseline="middle"
    >
      {data.data.name}
    </text>
  </g>
);

And in my pie chart component I removed the last code and then called the arcpPath component using the code below:

<svg width={width} height={height}>
  <g transform={`translate(${width / 2},${height / 2})`}>
    {totalData.map((d, i) => (
      <ArcPath
        key={i}
        data={d}
        index={i}
        createArc={createArc}
        colors={colors}
      />
    ))}
  </g>
</svg>
Clone this wiki locally