Recently I published a video on how to create a simple Line Chart from first principles 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.
This type of a line chart is useful when you want to show the progress over a week for example. Here I use a weeks worth of data to illustrate the solution.
Creating a line chart is not that hard, there are a few simple things that could trip you up, and I will describe them here. So let’s get into it.
Let’s start with the data structures. Minimum amount of information required for a line chart are a value for Y axis, and another value for X axis. We can prototype it this way.
So here, we define a structure called Score, which would contain a floating point value and a DateTime. And now, we can create a dummy dataset as follows. we use the random number generator to create some values and assign a date going back from the current date.
We define the data outside the LineChartWidget and pass into it, and that data is not mutated inside the LineChartWidget. This goes well with the composability of Flutter Framework.
LineChartWidget in its most basic form will look like this. We will improve upon it as we go along.
The scores are received in the constructor of this stateless widget and then eventually passes on to the LineChartPainter class. We are using a stateless widget here because we don’t perform any animations that require state to be persisted.
LineChartPainter is a derived class from CustomPaint and all the drawing will happen in there. So let’s take a look at how to implement that.
There’s a bit of processing going on here. We compute the range, the values can take so we can draw the chart zoomed into the range. Also we need to have the scores transformed into two arrays to be drawn, represented in _X and _Y. We map the weekday indices to a label as well.
With these handy, we can take a look at how to implement the paint method.
First thing to do is to take some measurements of the canvas area available for us to draw. drawableHeight and drawableWidth variables define an area after setting a border. The variables wu and hu define a unit for horizontal and vertical direction. With that we can say that the Y value range should take 3 height units (hu) , and the rest is for drawing labels. And each data item will get wu horizontal space for drawing. Essentially with boxW and boxH we define a box where the data point will be drawn in.
We also set up an escape if the passed in scores result in parameters that are not drawable.
With these basic parameters computed, we can start to look at how to place the points on the canvas.
The value hr represents the pixel to Y value ratio. This is handy because we can quickly compute the vertical pixel location by a simple multiplication. The lt represents the starting offset of the first point. Note above we defined a box for each data value with the variables boxW, and boxH. We draw the data value in the middle of the this box horizontally, that’s why the boxW/2.0.
We define two functions that will compute the points and the create the path and finally drawing it on to the canvas.
Let’s look at how to draw the lines and little circles for points.
In _computePoints function, we map each data point to a point in canvas. First is to compute the pixel Y value. This is given by yy. The pixel values increase going down in vertical direction, so we need to invert the drawing by subtracting from boxH. This will ensure that the chart is drawn right way up. The dp is the pixel position of the data value. The lt is incremented so we are moving towards right horizontally to compute the next point.
In _computePath function, we iterate through each point computed above and add them to a path. To do that, we should first move to the first position, and then draw a line to the next position.
In _drawDataPoints function, it iterate through each point computed above and draw two circles. One filled, and one with a border. This gives a nice visual effect.
Next let’s draw labels. First is to generate the Y labels for data points, and draw them. Next is to draw X axis labels showing the short date and weekday.
Let’s take a look at how this is actually done. There’s a fair bit of code, but it is actually quite simple.
The _computeLabels function simply converts the floating point data values to a string with 1 decimal point. The _drawYLabels function takes in the labels and the pixel points and iterate to combine both to position the label properly. It makes sure if our point is at the vertical edge, it is drawn inside the chart. Usually data label is drawn over the point. If it’s at the edge, the labels goes under.
For _drawXLabels it’s an easy iteration to go from one point to the other by adding boxW. This ensures that the label text is drawn centred horizontally, in the box we defined with boxW.
The next two functions are for drawing text on the canvas. I’ve described them in my earlier post. But I will do the same description, for completion.
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.
That’s all there’s to it. In the video I took a slightly different approach but arriving at the same destination. If you like to see the recorded live coding please checkout the video. Ask questions in the comments if anything is unclear.