Donut Charts in Flutter-img

Donut Charts in Flutter

Use charts_flutter to create a donut chart with all major functionalities
This guide will teach you how to create and customize pie/donut charts for your flutter application using the charts_flutter package.

Here is the list of features that our chart will have:
  • Different Colors per section
  • Legend
  • Labels
  • Title
  • Selection Call back
  • Initial Selection
  • how to turn your chart into a Guage

By the end of this guide you should have something that looks like this:

Getting Started

Feel free to use your project or create a new one with a Scaffold widget set up for this guide (I am assuming you know how to do that). We also need to add the charts_flutter package to our app. Please add
charts_flutter: ^0.9.0
to your pubspec.yaml. This is the latest version at the time of writing, please check the latest version here.

Let's create a new file donut_chart.dart that will store our chart. Now add the code below to create a stateless widget and import the package. This will be our starting point.
import 'package:flutter/material.dart';
import 'package:charts_flutter/flutter.dart' as charts;

class DonutPieChart extends StatelessWidget {
  final List<charts.Series> seriesList;
  final bool animate;

  DonutPieChart(this.seriesList, {this.animate});

  @override
  Widget build(BuildContext context) {
    return new charts.PieChart(
      seriesList,
      animate: animate,
    );
  }
}
  • charts.Series is a list of data points combined together with some config to draw on our chart. Some charts can have multiple series (imagine a line chart with more than one line)
  • animate is used to tell the chart to animate or not.

Sample data

Let's start by creating a class called Purchases that will have a category, a total amount, and a color for its section in the chart. Create a new file lib\models\purchases.dart;
import 'package:charts_flutter/flutter.dart' as charts;

class Purchases {
  final String category;
  final num amount;
  final charts.Color color;
  Purchases(this.category, this.amount, this.color);
}
We use the Color class from the charts library so we don't get a conflict later on when we're trying to add the color function to our Series. Don't forget to import the class into the chart widget file.

Now let's create a method that returns a seriesList with some preassigned values. Let's add this to our class:
static List<charts.Series<Purchases, String>> _createPurchaseData() {
  final data = [
    new Purchases("Eating Out", 100, charts.Color(r: 255, g: 89, b: 100)),
    new Purchases("Groceries", 75, charts.Color(r: 89, g: 255, b: 89)),
    new Purchases("Shopping", 25, charts.Color(r: 89, g: 216, b: 255)),
    new Purchases("Traveling", 5, charts.Color(r: 255, g: 166, b: 89)),
  ];

  return [
    new charts.Series<Purchases, String>(
      id: 'Purchases',
      domainFn: (Purchases purchases, _) => purchases.category,
      measureFn: (Purchases purchases, _) => purchases.amount,
      data: data,
    )
  ];
}
  • data is our purchases list with predefined values that will usually be fetched from an external source.

We return a list of one chart Series:
  • domainFn This defines the thing that we're measuring and is a function that has Purchases and an int as args. We will use category as our measure.
  • measureFn is also a function that defines the numerical value that we see in the graph. Here we will use our amount

Now let's add a factory constructor for our widget to use this sample data:
factory DonutPieChart.withSampleData() {
  return new DonutPieChart(
    _createPurchaseData(),
    animate: true,
  );
}
Now we need to turn it into a donut chart. It's simple all we need to do is add a defaultRenderer with an arc width to our PieChart.
Widget build(BuildContext context) {
  return new charts.PieChart(
    seriesList,
    animate: animate,
    // new code below
    defaultRenderer: new charts.ArcRendererConfig(
      arcWidth: 15,
    ),
  );
}
Let's add our chart to our main widget by placing it in a sizedBox inside our main Scaffold. (or to your own screen)
// we are using this as the home of our Material app
class ChartsPage extends StatelessWidget {
  const ChartsPage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Donut Chart"),
      ),
      body: Container(
        child: SizedBox(
          height: 400,
          child: DonutPieChart.withSampleData(),
        ),
      ),
    );
  }
}
We should now have a very basic Donut chart showing on the screen.

Colors

To make this a truely customizable chart we need to add colors. It's simple due to the fact that our model already has a variable to store the color that the chart will use. All we need to do is add colorFn to our series which is a function that is used to define the colors for each segement of the chart. It should look like this
new charts.Series<Purchases, String>(
  id: 'Purchases',
  domainFn: (Purchases purchases, _) => purchases.category,
  measureFn: (Purchases purchases, _) => purchases.amount,
  data: data,
  // new code here
  colorFn: (Purchases purchases, _) => purchases.color,
)
  • colorFn goes over the data list we provide and uses the return value of the function as the color.

Labels

Labels a part of almost every chart. There are 2 types of labels outside or inside. In this case, we will use outside labels because an inside label won't fit. To do that we need to modify our default renderer and add arcRendererDecorators
defaultRenderer: new charts.ArcRendererConfig(
  arcWidth: 15,
  // new code below
  arcRendererDecorators: [
    new charts.ArcLabelDecorator(
      showLeaderLines: false,
      outsideLabelStyleSpec: new charts.TextStyleSpec(fontSize: 18),
      // insideLabelStyleSpec: new charts.TextStyleSpec(fontSize: 18),
      labelPosition: charts.ArcLabelPosition.outside,
    )
  ],
),
  • showLeaderLines boolean to show a line that points to our label. false is used here so we do not squeeze the information.
  • outsideLabelStyleSpec & insideLabelStyleSpec are use to edit the text style of the label
  • labelPosition defines where the label should be shown. There is outside, inside and auto. Auto defaults to inside unless the label does not fit.

This will show us the category name of each section, let's change that so the labels show the amount of each. We need to add a labelAccessorFn to our series.
new charts.Series<Purchases, String>(
  id: 'Purchases',
  domainFn: (Purchases purchases, _) => purchases.category,
  measureFn: (Purchases purchases, _) => purchases.amount,
  data: data,
  colorFn: (Purchases purchases, _) => purchases.color,
  // new code below
  labelAccessorFn: (Purchases purchases, _) => '${purchases.amount}\$',
)
  • labelAccessorFn the return value for this function is a new label for each section.

Title

Let's add a title and a subtitle to our chart to customize it even further. We need to add behaviours to our chart. Behaviors are a list of features that a chart has. We'll see more later. Our build method should now look like this.
Widget build(BuildContext context) {
  return new charts.PieChart(
    seriesList,
    animate: animate,
    defaultRenderer: new charts.ArcRendererConfig(
      arcWidth: 15,
      arcRendererDecorators: [
        new charts.ArcLabelDecorator(
          showLeaderLines: false,
          outsideLabelStyleSpec: new charts.TextStyleSpec(fontSize: 18),
          insideLabelStyleSpec: new charts.TextStyleSpec(fontSize: 18),
          labelPosition: charts.ArcLabelPosition.outside,
        )
      ],
    ),
    behaviors: [
      new charts.ChartTitle(
        "Donut Chart tutorial",
        subTitle: "Major fucntionality explained",
        titleOutsideJustification: charts.OutsideJustification.start,
        behaviorPosition: charts.BehaviorPosition.top,
        innerPadding: 24,
      ),
    ],
  );
}
  • The first argument is the title string
  • subTitle is to add a subtitle to the chart
  • titleOutsideJustification & behaviorPosition are for controlling the justification and position of the title.
  • innerPadding we use this to add padding to the title so it does not overlap with the graph or labels

These are some of the functionalities that the title, I'll list a couple of others that are not in the sample code for reference:
  • titleStyleSpec & subTitleStyleSpec are for customizing the text style for each
  • titleDirection for the direction of the title text.
A chart can have multiple titles, all we need to do is add a new ChartTitle with a different position to the behaviors list.

Legend

Let's continue by adding a legend to show the categories we're displaying in our chart. We can do this by adding the legend behavior to our chart DatumLegend. It should look like this.
behaviors: [
  // our title behaviour
  new charts.DatumLegend(
    position: charts.BehaviorPosition.bottom,
    outsideJustification: charts.OutsideJustification.middleDrawArea,
    horizontalFirst: false,
    cellPadding: new EdgeInsets.only(right: 4.0, bottom: 4.0),
    showMeasures: true,
    desiredMaxColumns: 2,
    desiredMaxRows: 2,
    legendDefaultMeasure: charts.LegendDefaultMeasure.firstValue,
    measureFormatter: (num value) {
      return value == null ? '-' : "$value\$";
    },
    entryTextStyle: charts.TextStyleSpec(
        color: charts.MaterialPalette.black,
        fontFamily: 'Roboto',
        fontSize: 16),
  ),
],
  • postion, outsideJustification & insideJustification are used to position and justify the location of the legend.
  • horizontalFirst if true, legend entries will grow horizontally rather than vertically
  • desiredMaxColumns & desiredMaxRows are for defining the maximum number of columns and rows of your legend
  • cellPadding padding around each legend cell
  • showMeasures boolean value to tell the legend whether to show the measuring values or not
  • legendDefaultMeasure if measure value should show when there is no selection. in this case, we use first value because that is what we care about (this defaults to only showing selected values)
  • measureFormatter how the measure should be formatted if it's showing
  • entryTextStyle the text style of the label.
This might look like a lot and you definitely do not need all of these just to show a simple legend.

Selection call back

All we've done so far is customize the looks of the chart. Now let's look at how we can make it interact with other parts of the screen/application if we wanted to. This package allows us to add a call back to when a section or a series of the graph is selected.

Let's add a simple call back that prints the amount of the category we clicked to the console, to do that we need to add a selectionModel to our chart
behaviours: [
  // Our list of behaviours
]
selectionModels: [
  new charts.SelectionModelConfig(changedListener: (selectionModel) {
    final selectedData = selectionModel.selectedDatum;

    if (selectedData.isNotEmpty) {
      final purchases = selectedData.first.datum.amount;
      print(purchases);
    }
  }),
],
  • selectionModel takes a list of SelectionModelConfig that could define a change or update listeners that get triggered based on the event that happens.
  • changedListener takes a function and triggers it when a selection change happens. You can get the selected data as seen above by accessing the selectedDatum of the selectionModel from the callback

Initial Selection

Now that we've seen how to trigger a call back when a section is selected. Let's see how we can create a default selection for when the widget first builds and have it highlighted. This would need 2 new behaviors
behaviors: [
  // our previous behaviours
  new charts.InitialSelection(selectedDataConfig: [
    new charts.SeriesDatumConfig<String>('Purchases', 'Eating Out'),
  ]),
  new charts.DomainHighlighter(),
],
  • InitialSelection takes in a list of SeriesDatumConfig that point to our initial selection.
  • SeriesDatumConfig takes in an ID of the series and the selected domain (measure).
  • DomainHighlighter tell the chart to highlight the selected sections
This should now highlight the eating out selection and change the highlight whenever a different section is clicked.

Guage

This section is not needed if you're going for a full circle donut chart, but if you want to create a chart that doesn't cover a full circle then we would need to add a starting angle and an arc length to our defaultRenderer by using basic circle math.
defaultRenderer: new charts.ArcRendererConfig(
  arcWidth: 15,
  // new code below
  startAngle: 4 / 5 * 3.14,
  arcLength: 7 / 5 * 3.14,
  // new code above
  arcRendererDecorators: [
    new charts.ArcLabelDecorator(
        outsideLabelStyleSpec: new charts.TextStyleSpec(fontSize: 18),
        insideLabelStyleSpec: new charts.TextStyleSpec(fontSize: 18),
        labelPosition: charts.ArcLabelPosition.outside)
  ],
),

Conclusion

We now have a fully functional pie/donut chart that has the functionality you would expect in an application. Most if not all of the functionalities added to this chart can be added to bar, line, and time series charts that are all provided by the charts_flutter package. You can find out more by checking their charts gallery.

Like the post?

feel free to share it anywhere