Create animated donut chart using SVG and javascript

Created on

05 Feb 2019

Updated on

20 Jan 2021

There are many plugins to create awesome charts using SVG or canvas. But if all you want is a simple and beautiful doughnut chart that can animate, display information and be interactive, then this post is for you.

I will be using SVG to create the doughnut shape as event handling is easier on SVG than canvas.

I will show two methods to create the effect. One is to manually create the elements and the other is to use js to create and fill the elements.

Method 1

Using circle elements to create doughnut charts.

The below code is for creating a static doughnut chart with 4 items.

<div class="doughnut">
    <svg width="100%" height="100%" viewBox="0 0 100 100">
        <circle cx="50" cy="50" r="30" stroke="#80e080" stroke-width="15" fill="transparent" stroke-dasharray="188.496" stroke-dashoffset="141.372"  transform='rotate(-90 50 50)'/>
		<circle cx="50" cy="50" r="30" stroke="#4fc3f7" stroke-width="15" fill="transparent" stroke-dasharray="188.496" stroke-dashoffset="103.6728"  transform='rotate(0 50 50)'/>
		<circle cx="50" cy="50" r="30" stroke="#9575cd" stroke-width="15" fill="transparent" stroke-dasharray="188.496" stroke-dashoffset="169.6464"  transform='rotate(162 50 50)'/>
		<circle cx="50" cy="50" r="30" stroke="#f06292" stroke-width="15" fill="transparent" stroke-dasharray="188.496" stroke-dashoffset="150.7968"  transform='rotate(198 50 50)'/>
	</svg>
</div>

Which will produce the below output.

So how does it work?

We have four circle elements, each representing a part of the data to be shown. And each of these circle elements has a stroke-dasharray equal to its perimeter, which can be found out by using the formula 2 * π * r. In this case, the perimeter is 188.496.

The fill is set to transparent and a suitable stroke (color) is applied. stroke-width property is used to set the width of the doughnuts.

To fill each part of the doughnut to its respective amount, the stroke-dashoffset property is used. To find the required amount of stroke-dashoffset the below formula is used.

stroke-dashoffset = perimeter - perimeter * amount / 100.

Where amount is the amount to be filled in percentage.

Now we need to rotate each element such that they will start from the previous element’s end transform.

The value for rotation can be calculated using the formula below.

rotation = previousAmount * 360 / 100

Where previousAmount is the amount filled in the percentage of the previous element, 0 for the first element.

The second and third value in the transform is the start coordinate of the rotation. 50,50 will set it to the center.

In my example I used -90 as the base rotation instead of 0 because i like the fill to start at the top instead of the right side.
To do this, simply subtract 90 from the above equation.

Method 2

Use javascript to dynamically add the circle elements and automatically fill the values.

This method is more useful if you need to create a chart from unknown values or if you have too much data to be created manually.

Start with a div#doughnut

<div id="doughnut"></div>

We need only one element since all the other elements are added dynamically.

And in JS,

var data = [{
    fill:15,
    color:"#80e080"
},{
    fill:35,
    color:"#4fc3f7"
},{
    fill:20,
    color:"#9575cd"
},{
    fill:30,
    color:"#f06292"
}]

This is just a basic format for the data, it can be different from the above or contain more info.

Now we need to create and append the svg element to the container element.

var doughnut = document.querySelector("#doughnut");
var svg = document.createElement("svg");
svg.setAttribute("width","100%");
svg.setAttribute("height","100%");
svg.setAttribute("viewBox","0 0 100 100");
doughnut.appendChild(svg);

To create each part of the doughnut, the data object is utilized. We will create the required number of circle elements with proper color and fill amount and then append them to the svg .

var data = [{
    fill:15,
    color:"#80e080"
},{
    fill:35,
    color:"#4fc3f7"
},{
    fill:20,
    color:"#9575cd"
},{
    fill:30,
    color:"#f06292"
}]
var doughnut = document.querySelector("#doughnut"),
svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"),
filled = 0;
svg.setAttribute("width","100%");
svg.setAttribute("height","100%");
svg.setAttribute("viewBox","0 0 100 100");
doughnut.appendChild(svg);
data.forEach(function(o,i){
	var circle = document.createElementNS("http://www.w3.org/2000/svg","circle"),
	startAngle = -90,
	radius = 30,
	cx = 50,
	cy = 50,
	strokeWidth = 15,
	dashArray = 2*Math.PI*radius,
	dashOffset = dashArray - (dashArray * o.fill / 100),
	angle = (filled * 360 / 100) + startAngle;
	circle.setAttribute("r",radius);
	circle.setAttribute("cx",cx);
	circle.setAttribute("cy",cy);
	circle.setAttribute("fill","transparent");
	circle.setAttribute("stroke",o.color);
	circle.setAttribute("stroke-width",strokeWidth);
	circle.setAttribute("stroke-dasharray",dashArray);
	circle.setAttribute("stroke-dashoffset",dashOffset);
	circle.setAttribute("transform","rotate("+(angle)+" "+cx+" "+cy+")");
	svg.appendChild(circle);
	filled+= o.fill;
})

Live demo:

Fiddle link.

Adding animations.

For creating animations I will use the transition property. We first need to determine the total duration of the animation and then the duration is distributed among the circles with respect to their fill amount.

In order to create the animation effect, the stroke-dashoffset is initially set to the same as stroke-dasharray then the transition is added and stroke-dashoffset is set to its respective position.

var data = [{
    fill:15,
    color:"#80e080"
},{
    fill:35,
    color:"#4fc3f7"
},{
    fill:20,
    color:"#9575cd"
},{
    fill:30,
    color:"#f06292"
}]
var doughnut = document.querySelector("#doughnut"),
svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"),
filled = 0;
svg.setAttribute("width","100%");
svg.setAttribute("height","100%");
svg.setAttribute("viewBox","0 0 100 100");
doughnut.appendChild(svg);
data.forEach(function(o,i){
	var circle = document.createElementNS("http://www.w3.org/2000/svg","circle"),
	startAngle = -90,
	radius = 30,
	cx = 50,
	cy = 50,
	animationDuration = 2000,
	strokeWidth = 15,
	dashArray = 2*Math.PI*radius,
	dashOffset = dashArray - (dashArray * o.fill / 100),
	angle = (filled * 360 / 100) + startAngle,
	currentDuration = animationDuration * o.fill / 100,
	delay = animationDuration * filled / 100;
	circle.setAttribute("r",radius);
	circle.setAttribute("cx",cx);
	circle.setAttribute("cy",cy);
	circle.setAttribute("fill","transparent");
	circle.setAttribute("stroke",o.color);
	circle.setAttribute("stroke-width",strokeWidth);
	circle.setAttribute("stroke-dasharray",dashArray);
	circle.setAttribute("stroke-dashoffset",dashArray);
	circle.style.transition = "stroke-dashoffset "+currentDuration+"ms linear "+delay+"ms";
	circle.setAttribute("transform","rotate("+(angle)+" "+cx+" "+cy+")");
	svg.appendChild(circle);
	filled+= o.fill;
	setTimeout(function(){
		circle.style["stroke-dashoffset"] = dashOffset;
	},100);
})

The above shown is the entire code required to create an animated doughnut chart.

We declare the duration of the animation in the variable animationDuration and it is divided and distributed among each element.

After the element is appended, stroke-dashoffset is changed to its respective amount. Note that a setAnimation() function is used to make sure that the elements are appended and ready.

A live demo is shown below, you can also check this fiddle.

Do leave a comment if you have any doubts, suggestions or improvements.

Comments

Gravatar image

Charles

over 4 years ago

Great post. Thank you so much. A quick question though Is there a way to position a text round each of these arcs, I have been batteling with these for a long time now. Feels like theres a mathematical equation to do this. What I want to achieve is to texts at the center top of each arc, this text should always be centered irrespective of the arcs length. How do I get this done? Is there a mathematical thingy here? Or can I just attached a text on the undashed part of each arc? Thanks
Gravatar image

akzhy

over 4 years ago

Hey, You can place a text element at the center of each arc by calculating finding the center point on the arc. Check https://jsfiddle.net/km6wLe1o/1/ which shows a simple demo.
Gravatar image

alex

over 3 years ago

this is exactly what im looking for, thank you fo this mate

Gravatar image

Skyler

over 3 years ago

I see you added text. Is there a way add images instead of text in the center of each arc. This is exactly what I been looking for. Thanks for sharing this.

Gravatar image

akzhy

over 3 years ago

Hi, this is possible by using an image element instead of text, however for complex requirements I would recommend using any of the popular battle-tested libraries. Anyway here is a demo for it. https://jsfiddle.net/gh25wjc8/
Gravatar image

Ali

over 3 years ago

Hey excellent post, but can you please show me how to add a custom tooltip on every arc when hover on the arc. this will be a great help thanks.

Gravatar image

akzhy

over 3 years ago

Hi, since the arcs are actually circles, we can't accurately trigger mouse events on them, so showing a tooltip on hover may not be possible right now.
Gravatar image

Ali

over 3 years ago

ok then is there a possibility to show a gap or white space between every stroke?

Gravatar image

akzhy

over 3 years ago

Hey, we can't do that using circles, but it is possible using path elements. Here is a demo that I made https://jsfiddle.net/xs4c7yrq/2/

Post a comment

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

This website uses cookies to enhance the user experience. Privacy Policy