Making a progress circle in React
Tags: javascript, react, tutorial, datavis
Published: 2021-02-06 18:20:15

When I need something simple that I don't know how to build quickly, it can be tempting to find a library for it. But what is actually going on inside any given library? And how do I make it do this specific thing, instead of the 20 other options this library comes packaged with? I think it can sometimes take longer to read the docs and find workarounds than to just build the thing from scratch, and then at the end it's lighter and easier to make adjustments.

I recently wanted to make a progress circle / pie. It came out looking something like this:

![complete progress circle](https://dev-to-uploads.s3.amazonaws.com/i/vfbgqwem54zv64cof0qs.png)

Here is how I did it in React. The aim of this article is for people to do things their own way rather than using a component someone else has built for them, so I'd love to know what you'd do differently or additionally in the comments.

![ideas](https://media.giphy.com/media/chnHv7g2coez6ZEryb/giphy.gif)

## SVGs
I like SVGs a lot, they're awesome. They seem to have every attribute you could want and, unlike some CSS properties, they work on all main browsers. So actually we can do this whole thing without CSS. Since this is a dynamic component which will take a percentage value, we'll be using JavaScript to do all the calculations.

We're going to make two circles, one (blue) on top of the other (light grey). We'll use SVG's stroke-dasharray, which sets the length of a dashed stroke, and stroke-dashoffset, which sets where it begins relative to its natural starting point.

So the stroke dash length will be the circumference of the circle - `2 * pi * radius` - and the offset which we need to change for the blue circle will be a percentage of that. When we want to visualise 85%, that stroke will have to start at 15% of whatever the circumference is, so that we can only see the remaining 85% of the line before the dash ends.

```html
<svg width="200" height="200">
  <circle r="70" cx="100" cy="100" fill="transparent" stroke="lightgrey" stroke-width="2rem" stroke-dasharray="439.8" stroke-dashoffset="0"></circle>
  <circle r="70" cx="100" cy="100" fill="transparent" stroke="blue" stroke-width="2rem" stroke-dasharray="439.8" stroke-dashoffset="66"></circle>
</svg>
```

We're already off to a flying start, with some hard-coded values - in particular, the radius of `70`, the circumference of `439.8` and its "85%" bar starting at `66`. If you try this yourself, you'll see we're 90 degrees clockwise of where we'd want to be, and also missing the text value that should sit neatly in the middle. So we can put the circles in a group to rotate it -90 degrees, and add some text.


```html
<svg width="200" height="200">
  <g transform="rotate(-90 100 100)">
    <circle r="70" cx="100" cy="100" fill="transparent" stroke="lightgrey" stroke-width="2rem" stroke-dasharray="439.8" stroke-dashoffset="0"></circle>
    <circle r="70" cx="100" cy="100" fill="transparent" stroke="blue" stroke-width="2rem" stroke-dasharray="439.8" stroke-dashoffset="66"> 
    </circle>
  </g>
  <text x="50%" y="50%" dominant-baseline="central" text-anchor="middle">85%</text>
</svg>
```

There are some of those great SVG attributes I mentioned earlier - `dominant-baseline` and `text-anchor` helping us centre our text vertically and horizontally. Doing stuff like this in CSS can be a bit of a headache. When rotating SVGs we can also specify the centre of rotation - in this case its in the middle at `100 100`.

This actually already gives us the progress circle at the top of the article, so we're ready to move this to React.

## Making it a component

Using React gives us a lot of dynamic control over the values we're using. Let's take the percentage we want as an input, and the colour we want the progress to be.

We'll start by 'cleaning' the input to make sure it's a number we can use, we can set up the SVG parts as re-useable components and then we're basically done.

```jsx
const cleanPercentage = (percentage) => {
  const isNegativeOrNaN = !Number.isFinite(+percentage) || percentage < 0; // we can set non-numbers to 0 here
  const isTooHigh = percentage > 100;
  return isNegativeOrNaN ? 0 : isTooHigh ? 100 : +percentage;
};

const Circle = ({ colour, percentage }) => {
  const r = 70;
  const circ = 2 * Math.PI * r;
  const strokePct = ((100 - percentage) * circ) / 100; // where stroke will start, e.g. from 15% to 100%.
  return (
    <circle
      r={r}
      cx={100}
      cy={100}
      fill="transparent"
      stroke={strokePct !== circ ? colour : ""} // remove colour as 0% sets full circumference
      strokeWidth={"2rem"}
      strokeDasharray={circ}
      strokeDashoffset={percentage ? strokePct : 0}
    ></circle>
  );
};

const Text = ({ percentage }) => {
  return (
    <text
      x="50%"
      y="50%"
      dominantBaseline="central"
      textAnchor="middle"
      fontSize={"1.5em"}
    >
      {percentage.toFixed(0)}%
    </text>
  );
};

const Pie = ({ percentage, colour }) => {
  const pct = cleanPercentage(percentage);
  return (
    <svg width={200} height={200}>
      <g transform={`rotate(-90 ${"100 100"})`}>
        <Circle colour="lightgrey" />
        <Circle colour={colour} percentage={pct} />
      </g>
      <Text percentage={pct} />
    </svg>
  );
};
```

And actually this is just a starting point, since there are still hard-coded values - do we want to fix our radius to `70`, or stroke width to `2rem`, or circle size to `200`? I think probably not, and now that's all in our control - I've left curly braces wherever I would continue to add dynamic values. At the moment the component takes just percentage and colour, but it could take stroke width, radius, rounded ends and so on.

You can see the final code with some examples where I've added some more colours, rounded the ends using `stroke-linecap="round"` below; I've also included a "Randomise" button so you can see it in action.

{% codesandbox heuristic-burnell-kttl5 module=/Pie.jsx %}