Recently I published a video on how to create an animated donut chart using Flutter canvas and CustomPainter.
In this post I will describe the process in textual format for those of you who like to read than watch.
A donut chart is useful when you have a dataset that shows the constituents of a whole. Also the constituents should be discrete groups rather than a continuous value. In this example, I use a result of a poll for favourite movie genre with five genres to select from. Most commonly in the corporate world, you see donut charts showing the breakdown of revenue from all the products sold.
So this is a simple problem, but it’s important to think about how to solve it. Since each data item has a value that add up to the whole, we could create percentages out of these values, which all add up to 100%. And that 100% can be represented by 360 degrees. That’s the basis of a pie or donut chart.
So we can start to prototype this. We need a container for our data items.
With that handy now we can create a dummy dataset that we will use for drawing. This dataset will be passed into the DonutChartWidget. That’s the widget that would be responsible for taking the data and do all the computations necessary to draw and animate it. That’s a clean separation of concerns. So with that, the code looks like this.
Let me explain this a bit. Usually main.dart is the entry point to a Flutter app. In the main function we create a stateless widget called MyApp which builds a MaterialApp. This is the host of our widget. You may already have an app with various other functionality and with Flutter composability, it’s quite easy to add another widget to your app.
This is demonstrated by building a DonutChartWidget as a child of a container in the above code, note that we pass in the dataset to this widget.
Naturally we will go on to create the DonutChartWidget next. And in the most basic form, it would like as follows:
It receives the dataset in the constructor and we have marked it as final to denote that we won’t be changing it. What this means is that the dataset variable won’t be set to. This is slightly different from having an immutable dataset. Slight segue here: The elements of the dataset are references, and you could change the contents here if you want to. But that’s not what we want to do in this case.
Main purpose of DonutChartWidget as a stateful widget is for us to have mutable member variables for animation purposes. Or else, it could just as well be a stateless widget.
In the build method here, we casually create a CustomPaint object, and set a Container as the child and DonutChartPainter as the painter object. We will revisit this class again when we animate the drawing, but for now this sets up the widgets so we can draw something to the canvas. It’s important to set the Container as a child in the above code, this makes sure that the painter object gets finite sizes correctly.
Next let’s take a look at the heart of the matter DonutChartPainter. It is derived from CustomPainter class, which implements two methods: paint and shouldRepaint. In the paint method we do all our drawing. Let’s take a look at the first iteration of its implementation.
Just as before in the constructor DonutChartPainter receives the dataset. In the paint method, first we compute the centre of the canvas, define a radius to expand to almost to the width of canvas. With that we create a Rect object to be used for drawing the Arcs.
Once we have these peripheral variables set, it’s a matter of iterating the dataset items and for each item compute a sweep angle and draw a sector. We keep adding to the startAngle so that the next sector starts where the last one finished.
Now let’s take a look at the drawSector method.
In there, we set up a paint object using the data item colour and use drawArc to draw a sector. The 4th argument of drawArc toggles between drawing a sector or an arc. Note the use of rect, we defined previously. Rect defines the space in which the arc is drawn. From that drawArc automatically computes the centre and radius.
Next let’s take a look at how we can draw Text before examining the drawLabel.
Text drawing requires measuring and then painting. measureText function takes in the string to draw, a TextStyle, max width to use and an alignment. From this first it should create a TextSpan, which applies the style. The style defines things like the font colour, font face, font size, and font weight to use. From the TextSpan object a TextPainter object is created. This is where it applies the alignment and text direction. Once these settings are in place, we can call layout method on TextPainter object. This computes the area to draw the text we define with all the parameters.
Now that we have measured the text and know the space it takes to draw the letters, we can set the position it on the canvas, using the sizes returned by TextPainter object. calling the paint on TextPainter object finally draws on to the canvas.
You may have noticed that we are calling bgCb(Size sz) before calling the paint. This is a callback function we pass into the drawTextCentered so that we can draw a text background. For example, here in we use that to draw a filled round rectangle so that text displays well. Now we are in a position to look at drawLabels function.
We want to draw the labels in the middle of the sector, which means it should be half the sweepAngle, and at a distance r from the centre. We select r here such that it appear on the toroidal part of the donut not too close to the centre, as that might get obscured by the middle overlapping circle. When we have a distance and an angle from a point, we can always use polar coordinates to compute the cartesian coordinates of the position where the middle of the label should be. And that’s exactly what happens here. A simple polar to cartesian coordinate conversion gets us the position. We simply use the drawTextCentered with these parameters. Note the background drawing callback we are passing into the drawTextCentered. It creates a rounded rectangle before letters are drawn.
So this will get us drawing a static donut chart. Now it’s matter of animating this whole thing, which is quite simple. Remember when we started, we defined that 100% would represented by 360 degrees. We can just as easily use any value up to 360. So it’s a matter of updating the upper bound of fullAngle at a given tick so that it has the effect of rolling in.
For this we simply use a timer with a periodic timer, and in the callback we update the fullAngle by a step value. The code looks like this.
All of the animation happens inside the _DonutChartWidgetState. We create the timer inside the initState. As you can see in the timer callback, we compute the step and update the fullAngle. If the fullAngle reaches 360, we just cancel the timer. Also it is a good idea to dispose the timer, so that your app won’t crash when navigating away from the page showing this widget.
Note that we do our update inside a setState. This makes sure that the canvas is repainted at each fullAngle update.
That’s all there’s to it. If you like to see how this all happens live (almost), checkout the video. Happy to take questions or comments if you have any.