Simple bar chart with React and D3 📊
Tags: react, typescript, tutorial
Published: 2021-12-07 15:53:26

## Introduction

Recently I've started working on an expense tracking application for my personal use, and in order to visualize data better I've decided to add some bar charts to it.

I did some research and found a lot of helpful libraries e.g. [recharts](https://recharts.org/en-US/) or [react-vis](https://uber.github.io/react-vis/), but I thought for my case it would be an overkill, also it seems like a great opportunity to learn something new, so I've decided to use D3.

## What is D3?

D3 stands for Data-Driven Documents and as the [docs](https://d3js.org/) states:

> D3.js is a JavaScript library for manipulating documents based on data. D3 helps you bring data to life using HTML, SVG, and CSS.

After getting familiar with it, I got really excited by how powerful this library is and how many various cases this can help you to solve. Just check out this [gallery](https://observablehq.com/@d3/gallery) and tell me you're not impressed 😅

## Before we start

First things first, let's install D3 and its type declarations.

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

Also, let's initialize some dummy data to fill our chart.

```ts
interface Data {
  label: string;
  value: number;
}

const DATA: Data[] = [
  { label: "Apples", value: 100 },
  { label: "Bananas", value: 200 },
  { label: "Oranges", value: 50 },
  { label: "Kiwis", value: 150 }
];
```

Now we're ready to jump to the next section, so buckle up!

## Bar chart

Of course, we want our bar chart to be reusable through the whole application. To achieve that, let's declare it as a separate component that will take `data` prop and return SVG elements to visualize given data.

```tsx
interface BarChartProps {
  data: Data[];
}

function BarChart({ data }: BarChartProps) {
  const margin = { top: 0, right: 0, bottom: 0, left: 0 };
  const width = 500 - margin.left - margin.right;
  const height = 300 - margin.top - margin.bottom;

  return (
    <svg
      width={width + margin.left + margin.right}
      height={height + margin.top + margin.bottom}
    >
      <g transform={`translate(${margin.left}, ${margin.top})`}></g>
    </svg>
  );
}
```

Great, we have our SVG with declared `width` and `height` attributes. So far, so good. But you might wonder what is this `g` element for. Basically, you can think of it as a container for elements that will come next - x-axis, y-axis and bars that will represent our data. By manipulating its `transform` attribute with `margin` values, we will create some space to properly render all the above-mentioned elements.

### Bottom axis

Before we render our horizontal axis, we have to remember about scales. Scales are functions that are responsible for mapping data values to visual variables. I don't want to dive too deep into this topic, but if you're interested in further reading, you can check out scales [documentation](https://github.com/d3/d3-scale). We want our x-axis to display labels from data, so for this we will use `scaleBand`.

```ts
const scaleX = scaleBand()
  .domain(data.map(({ label }) => label))
  .range([0, width]);
```

Now we can create `AxisBottom` component which will render `g` element that will be used for drawing horizontal axis by calling `axisBottom` function on it.

```tsx
interface AxisBottomProps {
  scale: ScaleBand<string>;
  transform: string;
}

function AxisBottom({ scale, transform }: AxisBottomProps) {
  const ref = useRef<SVGGElement>(null);

  useEffect(() => {
    if (ref.current) {
      select(ref.current).call(axisBottom(scale));
    }
  }, [scale]);

  return <g ref={ref} transform={transform} />;
}
```

After using `AxisBottom` in our `BarChart` component, the code will look like this 👇

```tsx
export function BarChart({ data }: BarChartProps) {
  const margin = { top: 0, right: 0, bottom: 20, left: 0 };
  const width = 500 - margin.left - margin.right;
  const height = 300 - margin.top - margin.bottom;

  const scaleX = scaleBand()
    .domain(data.map(({ label }) => label))
    .range([0, width]);

  return (
    <svg
      width={width + margin.left + margin.right}
      height={height + margin.top + margin.bottom}
    >
      <g transform={`translate(${margin.left}, ${margin.top})`}>
        <AxisBottom scale={scaleX} transform={`translate(0, ${height})`} />
      </g>
    </svg>
  );
}
```

Notice how we added some bottom margin and set `transform` property of `AxisBottom` component to place it at the very bottom of our SVG container, since originally this would be rendered in the top-left corner.

Here's the result 👀

![SVG with bottom axis](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5v1tmqkjaj3zo22tp70y.png)

### Left axis

The process of creating the vertical axis is very similar to what we did earlier, but this time we will use `scaleLinear` for scale. On our y-axis, we want to display ticks for values from our data. Ticks are just "steps" between minimum and a maximum value in a given domain. To do that, we will pass `[0, max]` for our domain and `[height, 0]` for range. Notice how `height` goes first - it's because we want ticks to have maximum value on top of our y-axis, not at the bottom.

```ts
const scaleY = scaleLinear()
  .domain([0, Math.max(...data.map(({ value }) => value))])
  .range([height, 0]);
```

Now we're ready to start working on `AxisLeft` component. It's almost the same what we did in `AxisBottom` but this time we will use `axisLeft` function to draw our vertical axis.

```tsx
interface AxisLeftProps {
  scale: ScaleLinear<number, number, never>;
}

function AxisLeft({ scale }: AxisLeftProps) {
  const ref = useRef<SVGGElement>(null);

  useEffect(() => {
    if (ref.current) {
      select(ref.current).call(axisLeft(scale));
    }
  }, [scale]);

  return <g ref={ref} />;
}
```

After using it in `BarChart` the code will look like this 👇

```tsx
export function BarChart({ data }: BarChartProps) {
  const margin = { top: 10, right: 0, bottom: 20, left: 30 };
  const width = 500 - margin.left - margin.right;
  const height = 300 - margin.top - margin.bottom;

  const scaleX = scaleBand()
    .domain(data.map(({ label }) => label))
    .range([0, width]);
  const scaleY = scaleLinear()
    .domain([0, Math.max(...data.map(({ value }) => value))])
    .range([height, 0]);

  return (
    <svg
      width={width + margin.left + margin.right}
      height={height + margin.top + margin.bottom}
    >
      <g transform={`translate(${margin.left}, ${margin.top})`}>
        <AxisBottom scale={scaleX} transform={`translate(0, ${height})`} />
        <AxisLeft scale={scaleY} />
      </g>
    </svg>
  );
}
```

This time we added some top and left margin to make it visible on our SVG, but since it's initially placed in top-left corner we didn't have to set `transform` property.

Here's how it looks 👀

![SVG with left and bottom axes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s37cle2m2dzszwsgui0l.png)

### Bars

Time for rendering bars, it's my favourite part. In this component we will use `scaleX` and `scaleY` we declared earlier to compute `x`, `y`, `width` and `height` attributes for each value from our data. For rendering bar we will use SVG `rect` element.

```tsx
interface BarsProps {
  data: BarChartProps["data"];
  height: number;
  scaleX: AxisBottomProps["scale"];
  scaleY: AxisLeftProps["scale"];
}

function Bars({ data, height, scaleX, scaleY }: BarsProps) {
  return (
    <>
      {data.map(({ value, label }) => (
        <rect
          key={`bar-${label}`}
          x={scaleX(label)}
          y={scaleY(value)}
          width={scaleX.bandwidth()}
          height={height - scaleY(value)}
          fill="teal"
        />
      ))}
    </>
  );
}
```

After adding this to `BarChart` the final version of it will look like this 👇

```tsx
export function BarChart({ data }: BarChartProps) {
  const margin = { top: 10, right: 0, bottom: 20, left: 30 };
  const width = 500 - margin.left - margin.right;
  const height = 300 - margin.top - margin.bottom;

  const scaleX = scaleBand()
    .domain(data.map(({ label }) => label))
    .range([0, width])
    .padding(0.5);
  const scaleY = scaleLinear()
    .domain([0, Math.max(...data.map(({ value }) => value))])
    .range([height, 0]);

  return (
    <svg
      width={width + margin.left + margin.right}
      height={height + margin.top + margin.bottom}
    >
      <g transform={`translate(${margin.left}, ${margin.top})`}>
        <AxisBottom scale={scaleX} transform={`translate(0, ${height})`} />
        <AxisLeft scale={scaleY} />
        <Bars data={data} height={height} scaleX={scaleX} scaleY={scaleY} />
      </g>
    </svg>
  );
}
```

The things that changed is of course adding `Bars`, but besides that we used `padding` method on our `scaleX` to create some space between rectangles and improve chart readability.

## Demo

Feel free to fork this sandbox and play around with it. Maybe add separate colour for each bar, handle displaying negative values on it, add some more data, try to create horizontal bar chart etc.

{% codesandbox simple-bar-chart-kozpi %}

Also, if you would like to learn more I encourage you to check out this [tutorial](https://wattenberger.com/blog/react-and-d3) by Amelia Wattenberger, it's great.

Thanks for reading! 👋