There are plenty of articles on the topic, including two by Chris (here and here) and a super recent one by Burke Holland. I didn’t use D3 for this project because the application didn’t need the overhead of that library.
Like any self-respecting developer, the first thing I did was Google to see if someone else had already made this. Then, like same said developer, I scrapped the pre-built solution in favor of my own.
The top hit for “SVG donut chart” is this article, which describes how to use stroke-dasharray and stroke-dashoffset to draw multiple overlaid circles and create the illusion of a single segmented circle (more on this shortly).
I really like the overlay concept, but found recalculating both stroke-dasharray and stroke-dashoffset values confusing. Why not set one fixed stroke-dasharrary value and then rotate each circle with a transform? I also needed to add labels to each segment, which wasn’t covered in the tutorial.
Drawing a line
Before we can create a dynamic donut chart, we first need to understand how SVG line drawing works. If you haven’t read Jake Archibald’s excellent Animated Line Drawing in SVG. Chris also has a good overview.
Those articles provide most of the context you’ll need, but briefly, SVG has two presentation attributes: stroke-dasharray and stroke-dashoffset.
stroke-dasharray defines an array of dashes and gaps used to paint the outline of a shape. It can take zero, one, or two values. The first value defines the dash length; the second defines the gap length.
stroke-dashoffset, on the other hand, defines where the set of dashes and gaps begins. If the stroke-dasharray and the stroke-dashoffset values are the length of the line and equal, the entire line is visible because we’re telling the offset (where the dash-array starts) to begin at the end of the line. If the stroke-dasharray is the length of the line, but the stroke-dashoffset is 0, then the line is invisible because we’re offsetting the rendered part of the dash by its entire length.
To create the donut chart’s segments, we’ll make a separate circle for each one, overlay the circles on top of one another, then use stroke, stroke-dasharray, and stroke-dashoffset to show only part of the stroke of each circle. We’ll then rotate each visible part into the correct position, creating the illusion of a single shape. As we do this, we’ll also calculate the coordinates for the text labels.
Here’s an example demonstrating these rotations and overlays:
Create our Vue instance and our donut chart component, then tell our donut component to expect some values (our dataset) as props
Establish our basic SVG shapes: <circle> for the segments and <text> for the labels, with the basic dimensions, stroke width, and colors defined
Wrap these shapes in a <g> element, which groups them together
Add a v-for loop to the g> element, which we’ll use to iterate through each value that the component receives
Create an empty sortedValues array, which we’ll use to hold a sorted version of our data
Create an empty chartData array, which will contain our main positioning data
Our stroke-dasharray should be the length of the entire circle, giving us an easy baseline number which we can use to calculate each stroke-dashoffset value. Recall that the length of a circle is its circumference and the formula for circumference is 2πr (you remember this, right?).
We can make this a computed property in our component.
No segments. Just a solid-colored donut. Like HTML, SVG elements are rendered in the order that they appear in the markup. The color that appears is the stroke color of the last circle in the SVG. Because we haven’t added any stroke-dashoffset values yet, each circle’s stroke goes all the way around. Let’s fix this by creating segments.
To get each of the circle segments, we’ll need to:
Calculate the percentage of each data value from the total data values that we pass in
Multiply this percentage by the circumference to get the length of the visible stroke
Subtract this length from the circumference to get the stroke-offset
It sounds more complicated than it is. Let’s start with some helper functions. We first need to total up our data values. We can use a computed property to do this.
Each loop creates a new object with a “degrees” property, pushes that into our chartValues array that we created earlier, and then updates the angleOffset for the next loop.
But wait, what’s up with the -90 value?
Well, looking back at our original mockup, the first segment is shown at the 12 o’clock position, or -90 degrees from the starting point. By setting our angleOffset at -90, we ensure that our largest donut segment starts from the top.
To rotate these segments in the HTML, we’ll use the transform presentation attribute with the rotate function. Let’s create another computed property so that we can return a nice, formatted string.
The rotate function takes three arguments: an angle of rotation and x and y coordinates around which the angle rotates. If we don’t supply cx and cy coordinates, then our segments will rotate around the entire SVG coordinate system.
Next, we bind this to our circle markup.
And, since we need to do all of these calculations before the chart is rendered, we’ll add our calculateChartData computed property in the mounted hook:
We have our segments, but now we need to create labels. This means that we need to place our <text> elements with x and y coordinates at different points along the circle. You might suspect that this requires math. Sadly, you are correct.
Fortunately, this isn’t the kind of math where we need to apply Real Concepts; this is more the kind where we Google formulas and don’t ask too many questions.
According to the Internet, the formulas to calculate x and y points along a circle are:
x = r cos(t) + a y = r sin(t) + b
…where r is the radius, t is the angle, and a and b are the x and y center point offsets.
We already have most of this: we know our radius, we know how to calculate our segment angles, and we know our center offset values (cx and cy).
There’s one catch, though: in those formulas, t is in *radians*. We’re working in degrees, which means that we need to do some conversions. Again, a quick search turns up a formula:
First, we calculate the angle of our segment by multiplying the ratio of our data value by 360; however, we actually want half of this because our text labels are in the middle of the segment rather than the end. We need to add the angle offset like we did when we created the segments.
Our calculateTextCoords method can now be used in the calculateChartData computed property: