Build Data Visualizations with React
Tags: react, d3, graph, svg
Published: 2022-02-11 15:40:53

---
title: Build Data Visualizations with React
published: true
description: Create SVG Graphs and Visualizations in React using D3
tags: react, d3, graph, svg
canonical_url: https://nabeelvalley.co.za/blog/2022/11-02/d3-with-react
---

> If you want to take a look at the full code alongside the resulting SVG then head on over to [my website](https://nabeelvalley.co.za/blog/2022/11-02/d3-with-react)

React is a library for building reactive user interfaces using JavaScript (or Typescript) and D3 (short for _Data-Driven Documents_) is a set of libraries for working with visualizations based on data

Before getting started, I would recommend familiarity with SVG, React, and D3

Some good references for SVG are on the [MDN SVG Docs](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial)

A good place to start for React would be the [React Docs](https://reactjs.org/) or [my React Notes](/docs/react/complete-react.md)

And lastly, the [D3 Docs](https://github.com/d3/d3/wiki)

## Getting Stared

To follow along, you will need to install [Node.js](https://nodejs.org/en/) and be comfortable using the terminal

I'm going to be using a React App with TypeScript initialized with Vite as follows:

```sh
yarn create vite
```

And then selecting the `react-ts` option when prompted. Next, install `d3` from the project root with:

```sh
yarn add d3
yarn add --dev @types/d3
```

Now that we've got a basic project setup, we can start talking about D3

## Scales (`d3-scale`)

> [`d3-scale` Documentation](https://github.com/d3/d3-scale)

Broadly, scales allow us to map from one set of values to another set of values,

Scales in D3 are a set of tools which map a dimension of data to a visual variable. They help us go from something like `count` in our data to something like `width` in our rendered SVG

We can create scales for a sample dataset like so:

```ts
type Datum = {
  name: string
  count: number
}

export const data: Datum[] = [
  { name: "🍊", count: 21 },
  { name: "🍇", count: 13 },
  { name: "🍏", count: 8 },
  { name: "🍌", count: 5 },
  { name: "🍐", count: 3 },
  { name: "🍋", count: 2 },
  { name: "🍎", count: 1 },
  { name: "🍉", count: 1 },
]
```

> Also, a common thing to do when working with scales is to define margins around out image, this is done simply as an object like so:
> ```ts
> const margin = {
>   top: 20,
>   right: 20,
>   bottom: 20,
>   left: 35,
> };
> ```
> This just helps us simplify some position/layout things down the line

Scales work by taking a value from the `domain` (data space) and returning a value from `range` (visual space):

```ts
const width = 600;
const height = 400;

const x = d3
  .scaleLinear()
  .domain([0, 10])    // values of the data space
  .range([0, width])  // values of the visual space

const position = x(3) // position = scale(value)
```

Additionally, there's also the `invert` method which goes the other way - from `range` to `domain`

```ts
const position = x(3)      // position === 30
const value = x.invert(30) // value === 3
```

> The `invert` method is useful for things like calculating a `value` from a mouse `position`

D3 has different Scale types:

- Continuous (Linear, Power, Log, Identity, Time, Radial)
- Sequential
- Diverging
- Quantize
- Quantile
- Threshold
- Ordinal (Band, Point)

### Continuous Scales

These scales map continuous data to other continuous data

D3 has a few different continuous scale types: 

- Linear
- Power
- Log
- Identity
- Radial
- Time
- Sequential Color

For my purposes at the moment I'm going to be looking at the methods for Linear and Sequential Color scales, but the documentation explains all of the above very thoroughly and is worth a read for additional information on their usage

#### Linear

We can use a `linear` scale in the fruit example for mapping count to an x width:

```ts
const maxX = d3.max(data, (d) => d.count) as number;

const x = d3
  .scaleLinear<number>()
  .domain([0, maxX])
  .range([margin.left, width - margin.right]);
```

If we don't want the custom `domain` to `range` interpolation we can create a custom `interpolator`. An `interpolator` is a function that takes a value from the `domain` and returns the resulting `range` value

D3 has a few different `interpolators` included for tasks such as interpolating colors or rounding values

We can create a custom color domain to interpolate over and use the `interpolateHsl` or `interpolateRgb` functions:

```ts
const color = d3
  .scaleLinear<string>()
  .domain([0, maxX])
  .range(["pink", "lightgreen"])
  .interpolate(d3.interpolateHsl);
```

### Sequential Color

If for some reason we want to use the pre-included color scales

The `scaleSequential` scale is a method that allows us to map to a `color` range using an `interpolator`.

D3 has a few different interpolators we can use with this function like `d3.interpolatePurples`, `d3.interpolateRainbow` or `d3.interpolateCool` among others which look quite nice

We can create a color scale using the `d3.interpolatePurples` which will map the data to a scale of purples:

```ts
const color = d3
  .scaleSequential()
  .domain([0, maxX])
  .interpolator(d3.interpolatePurples);
```

These can be used instead of the `scaleLinear` with `interpolateHsl` for example above but to provide a pre-calibrated color scale

### Ordinal Scales

Ordinal scales have a discrete domain and range and are used for the mapping of discrete data. These are a good fit for mapping a scale with categorical data. D3 offers us the following scales:

- Band Scale
- Point Scale

#### Band Scale

A Band Scale is a type of Ordinal Scale where the output `range` is continuous and numeric

We can create a mapping for where each of our labels should be positioned with `scaleBand`:

```ts
const names = data.map((d) => d.name);

const y = d3
  .scaleBand()
  .domain(names)
  .range([margin.top, height - margin.bottom])
  .padding(0.1);
```

> The domain can be any size array, unlike in the case of continuous scales where the are usually start and end values

## Building a Bar Graph

When creating visuals with D3 there are a few different ways we can output to SVG data. D3 provides us with some methods for creating shapes and elements programmatically via a builder pattern - similar to how we create scales. 

However, there are also cases where we would want to define out SVG elements manually, such as when working with React so that the react renderer can handle the rendering of the SVG elements and we can manage our DOM structure in a way that's a bit more representative of the way we work in React


### The SVG Root

Every SVG image has to have an `svg` root element. To help ensure that this root scales correctly we also use it with a `viewBox` attribute which specifies which portion of the SVG is visible since the contents can go outside of the bounds of the View Box and we may not want to display this overflow content by default

Using the definitions for `margin`, `width` and `height` from before we can get the `viewBox` for the SVG we're trying to render like so:

```ts
const viewBox = `0 ${margin.top} ${width} ${height - margin.top}`;
```

And then, using that value in the `svg` element:

```tsx
return (
  <svg viewBox={viewBox}>
    {/* we will render the graph in here */}
  </svg>
)
```

At this point we don't really have anything in the SVG, next up we'll do the following:

1. Add Bars to the SVG
2. Add Y Labels to the SVG
3. Add X Labels to the SVG

### Bars

We can create Bars using the following:

```tsx
const bars = data.map((d) => (
  <rect
    key={y(d.name)}
    fill={color(d.count)}
    y={y(d.name)}
    x={x(0)}
    width={x(d.count) - x(0)}
    height={y.bandwidth()}
  />
));
```

We make use of the `x` and `y` functions which help us get the positions for the `rect` as well as `y.bandWidth()` and `x(d.count)` to `height` and `width` for the element

We can then add that into the SVG using:

```tsx
return (
  <svg viewBox={viewBox}>
    <g>{bars}</g>
  </svg>
);
```

### Y Labels

Next, using similar concepts as above, we can add the Y Labels:

```tsx
const yLabels = data.map((d) => (
  <text key={y(d.name)} y={y(d.name)} x={0} dy="0.35em">
    {d.name}
  </text>
));
```

Next, we can add this into the SVG, and also wrapping the element in a `g` with a some basic text alignment and translation for positioning it correctly:

```tsx
return (
  <svg viewBox={viewBox}>
    <g
      fill="steelblue"
      textAnchor="end"
      transform={`translate(${margin.left - 5}, ${y.bandwidth() / 2})`}
    >
      {yLabels}
    </g>
    <g>{bars}</g>
  </svg>
);
```

### X Labels

Next, we can add the X Labels over each `rect` using:

```tsx
const xLabels = data.map((d) => (
  <text key={y(d.name)} y={y(d.name)} x={x(d.count)} dy="0.35em">
    {d.count}
  </text>
));
```

And the resulting code looks like this:

```tsx
return (
  <svg viewBox={viewBox}>
    <g
      fill="steelblue"
      textAnchor="end"
      transform={`translate(${margin.left - 5}, ${y.bandwidth() / 2})`}
    >
      {yLabels}
    </g>
    <g>{bars}</g>
    <g
      fill="white"
      textAnchor="end"
      transform={`translate(-6, ${y.bandwidth() / 2})`}
    >
      {xLabels}
    </g>
  </svg>
);
```

### Final Result

The code for the entire file/graph can be seen below:

<details>
<summary>Fruit.tsx</summary>

```tsx
import React from "react";
import * as d3 from "d3";
import { data } from "../data/fruit";

const width = 600;
const height = 400;

const margin = {
  top: 20,
  right: 20,
  bottom: 20,
  left: 35,
};

const maxX = d3.max(data, (d) => d.count) as number;

const x = d3
  .scaleLinear<number>()
  .domain([0, maxX])
  .range([margin.left, width - margin.right])
  .interpolate(d3.interpolateRound);

const names = data.map((d) => d.name);

const y = d3
  .scaleBand()
  .domain(names)
  .range([margin.top, height - margin.bottom])
  .padding(0.1)
  .round(true);

const color = d3
  .scaleSequential()
  .domain([0, maxX])
  .interpolator(d3.interpolateCool);

export const Fruit: React.FC = ({}) => {
  const viewBox = `0 ${margin.top} ${width} ${height - margin.top}`;

  const yLabels = data.map((d) => (
    <text key={y(d.name)} y={y(d.name)} x={0} dy="0.35em">
      {d.name}
    </text>
  ));

  const bars = data.map((d) => (
    <rect
      key={y(d.name)}
      fill={color(d.count)}
      y={y(d.name)}
      x={x(0)}
      width={x(d.count) - x(0)}
      height={y.bandwidth()}
    />
  ));

  const xLabels = data.map((d) => (
    <text key={y(d.name)} y={y(d.name)} x={x(d.count)} dy="0.35em">
      {d.count}
    </text>
  ));

  return (
    <svg viewBox={viewBox}>
      <g
        fill="steelblue"
        textAnchor="end"
        transform={`translate(${margin.left - 5}, ${y.bandwidth() / 2})`}
      >
        {yLabels}
      </g>
      <g>{bars}</g>
      <g
        fill="white"
        textAnchor="end"
        transform={`translate(-6, ${y.bandwidth() / 2})`}
      >
        {xLabels}
      </g>
    </svg>
  );
};
```
</details>

### Ticks and Grid Lines

> Note that D3 includes a `d3-axis` package but that doesn't quite work given that we're manually creating the SVG using React and not D3

We may want to add Ticks and Grid Lines on the X-Axis, we can do this using the scale's `ticks` method like so:

```tsx
const xGrid = x.ticks().map((t) => (
  <g key={t}>
    <line
      stroke="lightgrey"
      x1={x(t)}
      y1={margin.top}
      x2={x(t)}
      y2={height - margin.bottom}
    />
    <text fill="darkgrey" textAnchor="middle" x={x(t)} y={height}>
      {t}
    </text>
  </g>
));
```

And then render this in the `svg` as:

```tsx
return (
<svg viewBox={viewBox}>
  <g>{xGrid}</g>
  { /* previous graph content */ }
</svg>
);
```

## Building a Line Graph

We can apply all the same as in the Bar Graph before to draw a Line Graph. The example I'll be using consists of a `Datum` as follows:

```ts
export type Datum = {
  date: Date;
  temp: number;
};
```

Given that the X-Axis is a `DateTime` we will need to do some additional conversions as well as formatting

### Working with Domains

In the context of this graph it would also be useful to have an automatically calculated domain instead of a hardcoded one as in the previous example

We can use the `d3.extent` function to calculate a domain:

```ts
const dateDomain = d3.extent(data, (d) => d.date) as [Date, Date];
const tempDomain = d3.extent(data, (d) => d.temp).reverse() as [number, number];
```

We can then use this domain definitions in a `scale`:

```ts
const tempScale = d3
  .scaleLinear<number>()
  .domain(tempDomain)
  .range([margin.top, height - margin.bottom])
  .interpolate(d3.interpolateRound);

const dateScale = d3
  .scaleTime()
  .domain(dateDomain)
  .range([margin.left, width - margin.right]);
```

### Create a Line

The `d3.line` function is useful for creating a `d` attribute for an SVG `path` element which defines the line segments

The `line` function requires `x` and `y` mappings. The line for the graph path can be seen as follows:

```ts
const line = d3
  .line<Datum>()
  .x((d) => dateScale(d.date))
  .y((d) => tempScale(d.temp))(data) as string;
```

We also include the `Datum` type in the above to scope down the type of `data` allowed in the resulting function

### Formatting

D3 includes functions for formatting `DateTime`s. We can create a formatter for a `DateTime` as follows:

```ts
const formatter = d3.timeFormat("%Y-%m")
```

We can then use the formatter like so:

```ts
formatter(dateTime)
```

### Grid Lines

We can define the X Axis and grid lines similar to how we did it previously:

```tsx
const xGrid = dateTicks.map((t) => (
  <g key={t.toString()}>
    <line
      stroke="lightgrey"
      x1={dateScale(t)}
      y1={margin.top}
      x2={dateScale(t)}
      y2={height - margin.bottom}
      strokeDasharray={4}
    />
    <text fill="darkgrey" textAnchor="middle" x={dateScale(t)} y={height}>
      {formatter(t)}
    </text>
  </g>
));
```

And the Y Axis grid lines:

```tsx
const yGrid = tempTicks.map((t) => (
  <g key={t.toString()}>
    <line
      stroke="lightgrey"
      y1={tempScale(t)}
      x1={margin.left}
      y2={tempScale(t)}
      x2={width - margin.right}
      strokeDasharray={4}
    />
    <text
      fill="darkgrey"
      textAnchor="end"
      y={tempScale(t)}
      x={margin.left - 5}
    >
      {t}
    </text>
  </g>
));
```

### Final result

Using all the values that have been defined above, we can create the overall graph and grid lines like so:

```tsx
return (
  <svg viewBox={viewBox}>
    <g>{xGrid}</g>
    <g>{yGrid}</g>
    <path d={line} stroke="steelblue" fill="none" />
  </svg>
);
```

Unfortanately, since DEV seems to be breaking my code samples, you'll have to jump over to my website if you want to see the final code and SVG samples [here](https://nabeelvalley.co.za/blog/2022/11-02/d3-with-react)