Build your React charts without a library
Tags: react, charts, webdev, svg
Published: 2020-04-01 15:04:20

---
title: Build your React charts without a library
published: true
description: Building custom charts in React apps with pure SVG
tags: react, charts, webdev, svg
---

So you want to build a chart in your React app? If you're like me, the first thing you did was search for something like "react charting library".

There are lots of great lists out there debating the pros and cons of different options like [recharts](https://github.com/recharts/recharts) and [victory](https://github.com/FormidableLabs/victory). A lot of them are built on top of [D3](https://github.com/d3/d3). These libraries can get you up and running really quickly with powerful features, and for many use cases will be the right choice.

**But here I want to show you how quickly we can build our own charts without any of these libraries.** Since we already have React to manage the DOM, we'll just be using [SVG](https://developer.mozilla.org/en-US/docs/Web/SVG) to render our charts.

Why are we doing this?

- Every new library you use requires learning how it works. But SVG is built into the browser - it's not going anywhere or being updated any time soon!
- You can build your chart to your exact design requirements - no need to worry about whether the library supports what you want to do or not.
- Save your precious kB! Sure, building your custom chart may require more lines of code in your app than using a library, but the total bundle size should be much smaller that way.

> To go further with saving kB, the examples below all work with [Preact](https://preactjs.com/) too.

If you want to follow along, I'm starting with [create-react-app](https://github.com/facebook/create-react-app) using the TypeScript template:

```bash
npx create-react-app my-chart --template typescript
```

> I recommend quickly [adding Prettier for formatting too](https://twitter.com/edsbentley/status/1245271596429979648).

## Chart Axes

In the diagram of the chart axes we're about to build below, I've added some variables to help visualise. We put `y0` at the top since in SVG the y axis goes from top to bottom. You kind of have to look at everything upside down.

![axes-diagram](https://dev-to-uploads.s3.amazonaws.com/i/2hwjsybq1jx0slri1a9l.png)

Open `App.tsx` and replace it with the following (I'll explain the code in more detail later on):

```tsx
import React from "react";
import "./App.css";

const SVG_WIDTH = 400;
const SVG_HEIGHT = 300;

function App() {
  const x0 = 50;
  const xAxisLength = SVG_WIDTH - x0 * 2;

  const y0 = 50;
  const yAxisLength = SVG_HEIGHT - y0 * 2;

  const xAxisY = y0 + yAxisLength;

  return (
    <svg width={SVG_WIDTH} height={SVG_HEIGHT}>
      {/* X axis */}
      <line
        x1={x0}
        y1={xAxisY}
        x2={x0 + xAxisLength}
        y2={xAxisY}
        stroke="grey"
      />
      <text x={x0 + xAxisLength + 5} y={xAxisY + 4}>
        x
      </text>

      {/* Y axis */}
      <line x1={x0} y1={y0} x2={x0} y2={y0 + yAxisLength} stroke="grey" />
      <text x={x0} y={y0 - 8} textAnchor="middle">
        y
      </text>
    </svg>
  );
}

export default App;
```

And with that, we have an x and y axis!

![x-y-axis](https://dev-to-uploads.s3.amazonaws.com/i/lmv0eqi3xzfu7cjf9x0y.png)

To start with, we must put everything in the [`svg`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg) element:

```tsx
    <svg width={SVG_WIDTH} height={SVG_HEIGHT}>
      ...
    </svg>
```

You'll notice all SVG elements work just the same as other DOM elements like `div` and `p` in React. For `svg` make sure to pass in a width and height.

> Here I'm hardcoding the width and height, but for a responsive view this could be a prop in your component. There's a good example of measuring the width of the parent element in the [React Hooks FAQ](https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node).

Next is the [`line`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line) element:

```tsx
      <line
        x1={x0}
        y1={xAxisY}
        x2={x0 + xAxisLength}
        y2={xAxisY}
        stroke="grey"
      />
```

It's rather simple: draw a line from a point `(x1, y1)` to `(x2, y2)`. At a minimum, we also need a `stroke` colour. If you read the docs you'll find far more ways to customise it, such as getting a dashed line or changing the thickness. But a thin solid line (the default) is good enough for now.

Lastly we have the [`text`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text) element:

```tsx
      <text x={x0 + xAxisLength + 5} y={xAxisY + 4}>
        x
      </text>
```

As well as its content, it just needs an `x` and `y` coordinate. You'll notice for the `y` `text` I also included the `textAnchor="middle"` attribute to center the text.

## Bar chart

![bar-plot](https://dev-to-uploads.s3.amazonaws.com/i/y9q5eqmu47ynmk6bxwaa.png)

To produce this chart, replace your code with the following:

```tsx
import React from "react";
import "./App.css";

const SVG_WIDTH = 400;
const SVG_HEIGHT = 300;

const data: [string, number][] = [
  ["Mon", 12],
  ["Tue", 14],
  ["Wed", 12],
  ["Thu", 4],
  ["Fri", 5],
  ["Sat", 18],
  ["Sun", 0],
];

function App() {
  const x0 = 50;
  const xAxisLength = SVG_WIDTH - x0 * 2;

  const y0 = 50;
  const yAxisLength = SVG_HEIGHT - y0 * 2;

  const xAxisY = y0 + yAxisLength;

  const dataYMax = data.reduce(
    (currMax, [_, dataY]) => Math.max(currMax, dataY),
    -Infinity
  );
  const dataYMin = data.reduce(
    (currMin, [_, dataY]) => Math.min(currMin, dataY),
    Infinity
  );
  const dataYRange = dataYMax - dataYMin;

  const numYTicks = 5;

  const barPlotWidth = xAxisLength / data.length;

  return (
    <svg width={SVG_WIDTH} height={SVG_HEIGHT}>
      {/* X axis */}
      <line
        x1={x0}
        y1={xAxisY}
        x2={x0 + xAxisLength}
        y2={xAxisY}
        stroke="grey"
      />
      <text x={x0 + xAxisLength + 5} y={xAxisY + 4}>
        Day
      </text>

      {/* Y axis */}
      <line x1={x0} y1={y0} x2={x0} y2={y0 + yAxisLength} stroke="grey" />
      {Array.from({ length: numYTicks }).map((_, index) => {
        const y = y0 + index * (yAxisLength / numYTicks);

        const yValue = Math.round(dataYMax - index * (dataYRange / numYTicks));

        return (
          <g key={index}>
            <line x1={x0} y1={y} x2={x0 - 5} y2={y} stroke="grey" />
            <text x={x0 - 5} y={y + 5} textAnchor="end">
              {yValue}
            </text>
          </g>
        );
      })}
      <text x={x0} y={y0 - 8} textAnchor="middle">
        $
      </text>

      {/* Bar plots */}
      {data.map(([day, dataY], index) => {
        const x = x0 + index * barPlotWidth;

        const yRatio = (dataY - dataYMin) / dataYRange;

        const y = y0 + (1 - yRatio) * yAxisLength;
        const height = yRatio * yAxisLength;

        const sidePadding = 10;

        return (
          <g key={index}>
            <rect
              x={x + sidePadding / 2}
              y={y}
              width={barPlotWidth - sidePadding}
              height={height}
            />
            <text x={x + barPlotWidth / 2} y={xAxisY + 16} textAnchor="middle">
              {day}
            </text>
          </g>
        );
      })}
    </svg>
  );
}

export default App;
```

Let's break this down. At the top we have some mock `data`, with a value (I'm going to say $) for each day of the week. Then we need to calculate some values based on the data for plotting:

```tsx
  const dataYMax = data.reduce(
    (currMax, [_, dataY]) => Math.max(currMax, dataY),
    -Infinity
  );
  const dataYMin = data.reduce(
    (currMin, [_, dataY]) => Math.min(currMin, dataY),
    Infinity
  );
  const dataYRange = dataYMax - dataYMin;

  const numYTicks = 5;

  const barPlotWidth = xAxisLength / data.length;
```

For `dataYMax` and `dataYMin` we need to iterate through the data to calculate the values. This would certainly be a good place to add `useMemo` and extract into a utility function. Below that we calculate some more values we'll need for our chart.

For the y axis I've added some ticks along the axis:

```tsx
      {/* Y axis */}
      <line x1={x0} y1={y0} x2={x0} y2={y0 + yAxisLength} stroke="grey" />
      {Array.from({ length: numYTicks }).map((_, index) => {
        const y = y0 + index * (yAxisLength / numYTicks);

        const yValue = Math.round(dataYMax - index * (dataYRange / numYTicks));

        return (
          <g key={index}>
            <line x1={x0} y1={y} x2={x0 - 5} y2={y} stroke="grey" />
            <text x={x0 - 5} y={y + 5} textAnchor="end">
              {yValue}
            </text>
          </g>
        );
      })}
      <text x={x0} y={y0 - 8} textAnchor="middle">
        $
      </text>
```

Try to think through all of the coordinates in your head to understand how the values of `y` and `yValue` are determined, and see if the diagram below helps. You need to keep in mind that the y axis in our plot is bottom to top, but we are plotting the `line`'s `y` points in a top to bottom system.

![y-calc](https://dev-to-uploads.s3.amazonaws.com/i/n95px7r74cxp8lfpaiks.png)

The only new element here is [`g`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g), which is used to group SVG elements. It's also handy for adding a `key`.

Lastly we have the bar plots themselves:

```tsx
      {/* Bar plots */}
      {data.map(([day, dataY], index) => {
        const x = x0 + index * barPlotWidth;

        const yRatio = (dataY - dataYMin) / dataYRange;

        const y = y0 + (1 - yRatio) * yAxisLength;
        const height = yRatio * yAxisLength;

        const sidePadding = 10;

        return (
          <g key={index}>
            <rect
              x={x + sidePadding / 2}
              y={y}
              width={barPlotWidth - sidePadding}
              height={height}
            />
            <text x={x + barPlotWidth / 2} y={xAxisY + 16} textAnchor="middle">
              {day}
            </text>
          </g>
        );
      })}
```

The maths to calculate the right values does get a bit tricky, try working your way through it. I've also added some manual padding between the bars.

> Mike Bostock actually has [a good post on why D3 is still useful for these kind of calculations](https://medium.com/@mbostock/why-you-should-use-d3-ae63c276e958). I don't disagree, though personally I would prefer to write the utility functions myself, rather than bringing in more dependencies. But it's really up to you.

We're using the [`rect`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect) element here, which produces a rectangle, and whose properties are conveniently self explanatory. The only thing to keep in mind is the `x` and `y` coordinates refer to the top-left corner of the rectangle.

## Styling

![bar-plot-colour](https://dev-to-uploads.s3.amazonaws.com/i/1g3b11c0kgck2xpby8ch.png)

Open up `App.css`, and replace the contents with the following to adjust the colours and text size:

```css
text {
  font-size: .7em;
  fill: grey;
}

rect {
  fill: blue;
}
```

Here I'm using element selectors, but you can add class names to SVG elements exactly the same way you would do to a `div`. The biggest difference to note is we use `fill` to change the colour.

## What's next?

At this point you may be concerned that you have 100 lines of code to produce a, lets be honest, fairly ugly bar chart. But using just `line`, `text` and `rect` we can already go a long way! Other useful elements to check out are [`circle`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/circle) and [`path`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path). These building blocks are enough to produce the most vivid and captivating charts you can imagine.

SVG elements can be animated with CSS just like any other element. In React they also work great in that you can add `onClick` or `onPointerOver` attributes to make them interactive. They'll work exactly as you expect.

> Tip: sometimes your hover won't work on an element if other elements are laid on top of it. In this case, using an invisible element as the last element can get around this.

Sure, if you just need to plot some data in a generic way then reach for a library. But if you have a specific design in mind (or someone else designed in a mockup), using SVG directly enables you to build exactly what you want, without any compromises.

## Quick ref

Here's a quick reference covering the SVG elements you need:

- [`<svg>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg) - wrap all your elements in this
- [`<line>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line) - a line!
- [`<text>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text) - some text!
- [`<rect>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect) - a rectangle!
- [`<g>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g) - group elements together
- [`<circle>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/circle) - a circle!
- [`<path>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path) - define a shape
- [and more](https://developer.mozilla.org/en-US/docs/Web/SVG/Element)