Recently I was working on a project to improve our web app’s billing reports with pie charts that visualize customer usage. I wanted an implementation for the job with minimal foreseeable maintenance that kept our third-party dependencies low.
I thought a quick search on Stack Overflow would yield a copy/paste-able solution, but discovered there isn’t an agreed-upon approach.
Of the various approaches I saw, there are these three that caught my attention, and they all happen to use just HTML and CSS. Each approach has its own merits, and you’ll need to find the right approach for your use case.
Here’s a quick summary of what the implementation of the three approaches looks like:
Using CSS conic-gradient
function
//HTML
<div id="shape">
</div>
// CSS
#shape {
width: 300px;
height: 300px;
border-radius: 9999px;
background: conic-gradient(
LemonChiffon 0%, LemonChiffon 10%,
LightGreen 10%, LightGreen 30%,
WhiteSmoke 30%, WhiteSmoke 100%
);
}
Using SVG <circle>
element
// HTML
<div id="shape">
<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="25" fill="transparent" stroke-width="50" stroke-dashoffset="39.2" stroke-dasharray="15.7 141.3" stroke="LemonChiffon"></circle>
<circle cx="50" cy="50" r="25" fill="transparent" stroke-width="50" stroke-dashoffset="23.5" stroke-dasharray="31.4 125.6" stroke="LightGreen"></circle>
</svg>
</div>
// CSS
#shape svg {
width: 300px;
height: 300px;
border-radius: 9999px;
background-color: WhiteSmoke;
}
Using SVG <path>
//HTML
<div id="shape">
<svg viewBox="0 0 100 100">
<path d="M50,50 L50,0 A50,50,0,0,1,79.5,9.5 Z" fill="LemonChiffon"></path>
<path d="M50,50 L79.5,9.5 A50,50,0,0,1,94,73.5 Z" fill="LightGreen"></path>
</svg>
</div>
// CSS
#shape svg {
width: 300px;
height: 300px;
border-radius: 9999px;
background-color: WhiteSmoke;
}
Each approach has its pros and cons, and offers different types of versatility:
conic-gradient
approach allows you to not deal with SVG at all. Very little maths is needed but at the cost of slightly less browser compatibility compared to the other solutions.- The
<circle>
element solution requires you to understand how all of its parameters work. But once you’ve got those figured out, you can stack and control pie pieces individually. You can also use the same implementation to turn your pie charts into donut charts. - The
<path>
element solution requires you to understand how to calculate paths, so you need to spend some time learning the basics of SVG. But once you’re comfortable with plotting, you aren’t restricted to the shapes you want to draw. This solution allows highest customization to your chart, like giving the pie slices borders or using background images for each slice.
The SVG <circle>
element solution came out on top for me and for our use case: we currently don’t need chart animations/interactions and having the same implementation for both pie charts and donut charts is pretty useful.
The important thing to know about the <circle>
solution is that it takes advantage of drawing overlapping circles (one for each segment of the pie chart) and that we use the `dash` feature of SVG to draw each segment. Dashes are normally used to define how to represent breaks in lines, but you can also use them in other SVG objects like circles, and by defining the dashes carefully we can have one dash per circle to generate each slice of the pie chart.
Here’s a quick overview of how the <circle>
example above works.
- Start by defining the
viewBox
to be0 0 100 100
to simplify the calculation, fits a circle of 50 units radius. You can usewidth
andheight
to define the size of the pie chart. TheviewBox
is the canvas we’re going to be drawing on. - For the circles you want to draw, you set the radius value to the half of the outcome circle’s radius (that is
r="25"
instead of 50) and set thestroke-width
to be half the length of theviewBox
(that isstroke-width="50"
). By doing this, you will be drawing border of the circle with thickness equals to the length of the outcome circle’s radius, thus creating a solid circle. - Use
stroke-dasharray
to set the dash length and gap size between strokes. - The dash length will be the percentage of the circumference. For example, to draw 10% pie, it will be
0.1 * (2 * PI * 25) = 15.7
- To ensure the border dash appears only once set the gap size to be the circumference minus the dash length (that is
157 - 15.7 = 141.3
). - To draw the first slice of 10% use the percentage of the circumference and the gap size we just calculated with the expression
stroke-dasharray="15.7 141.3"
- Rotate the starting point from the default
(100, 50)
by 90 degrees or 25% of the circumferencestroke-dashoffset="39.2"
. - If the pie is not starting at the 0% mark, then take the percentage of rotation into the calculation. Example, to start at 10% mark, the
stroke-dashoffset
will be(0.25 - 0.1) * CIRCUMFERENCE = 23.5
. - To fill up any of the remaining area left, apply
border-radius
andbackground-color
on<svg>
element.
We can implement this as a React component to make it easier for other contributors to reuse:
const CIRCLE_RADIUS = 50 / 2;
const STROKE_WIDTH = CIRCLE_RADIUS * 2;
const CIRCUMFERENCE = 2 * Math.PI * CIRCLE_RADIUS;
const DASH_OFFSET = CIRCUMFERENCE / 4;
const PieChart = ({
backgroundColor = 'WhiteSmoke',
strokeColorOptions = ['LemonChiffon', 'LightGreen'],
values = [],
}) => {
const outputStrokeColors = _.times(values.length).map(
(i) => strokeColorOptions[i % strokeColorOptions.length]
);
return (
<svg
backgroundColor={backgroundColor}
viewBox="0 0 100 100"
width="300px"
height="300px"
borderRadius="9999px"
>
{values.map((val, index) => {
const strokeColor =
outputStrokeColors[index % outputStrokeColors.length];
const dashSize = (val / 100) * CIRCUMFERENCE;
const gapSize = CIRCUMFERENCE - dashSize;
// NOTE: if it's a full circle, then no need to provide a stroke dasharray
const strokeDasharray =
val === 100 ? 'none' : `${dashSize} ${gapSize}`;
const accumulatedPriorPercentage = _.sum(values.slice(0, index));
const relativeOffset =
(accumulatedPriorPercentage / 100) * CIRCUMFERENCE;
const adjustedOffset = DASH_OFFSET - relativeOffset;
return (
<circle
key={index}
cx={50}
cy={50}
fill="transparent"
r={CIRCLE_RADIUS}
stroke={strokeColor}
strokeDasharray={strokeDasharray}
strokeDashoffset={adjustedOffset}
strokeWidth={STROKE_WIDTH}
/>
);
})}
</svg>
);
};
There might be a point where requirements change and we’ll need to adopt another solution or revisit existing libraries. Until then, the lightweight and in-scope versatility of the SVG <circle>
solution will serve us well.