Custom styled Bar Chart with Flutter using charts_flutter

Today I'm going to show you can customize your bar chart a bit using the chart_flutter package. I show you how you can style bars and labels, taking inspiration from series The Witcher.

Custom styled Bar Chart with Flutter using charts_flutter
Photo by Safar Safarov / Unsplash

There are several libraries for crating charts in Flutter. One of them is charts_flutter, currently version 0.12.0 with null-safety available on pub.dev repository. This library allows us to create many kinds of charts that you can view in their online gallery.

The problem with charts_flutter is a slight lack of documentation on how to adapt the charts to our projects. πŸ˜₯ Β 

Today I'm going to show you how you can do it and style bars and labels, taking inspiration from my favorite series The Witcher.

Step. 0 – pre-config

In order for our chart to be modeled on the The Witcher series, we need at least a special font (and maybe two swords πŸ˜†) that we will be using. You can google it and download it from any source, I used that.

Then we need to load and install our font in the project:

  • Create an assets folder with a fonts subfolder in the root directory
  • Download the font package and move .ttf file to the assets/fonts folder
  • In pubspec.yaml, uncomment the asset key and add the fonts path:
assets:
    - assets/fonts/
  • For the fonts key, also add the path to the font and give it a name that you will use later in the rest of the code:
fonts:
    - family: thewitcher
      fonts:
        - asset: assets/fonts/Thewitcher-jnOj.ttf

Step. 1 – Scaffolding

Now that the pre-configuration is complete, we can import and call our bar chart at the place we choose (main.dart in my case). We do this with sample data CustomBarChart.withSampleData ()

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: SafeArea(
          child: Column(
            children: <Widget>[
              const Text(
                'The Witcher bar chart',
                style: TextStyle(
                  fontFamily: 'thewitcher',
                  fontSize: 18,
                ),
              ),
              WitcherChart.withSampleData(),
            ],
          ),
        ),
      ),
    );
  }
}

Step 2 - Sample data

To feed the bar chart, we will need to create our sample data, and for this we will need a simple class:

class BarData {
  final int coins;
  final int monster;

  const BarData(this.coins, this.monster);
}

Then create a list variable of type BarData with any numbers you want (important! First value - coins must start at 1 and increase by 1):

static final data = <BarData>[
  const BarData(1, 11),
  const BarData(2, 14),
  const BarData(3, 8),
  const BarData(4, 10),
  const BarData(5, 15),
  const BarData(6, 1),
  const BarData(7, 9),
  const BarData(8, 9),
  const BarData(8, 3),
  const BarData(10, 12),
  const BarData(11, 18),
  const BarData(12, 8),
  const BarData(13, 3),
  const BarData(14, 4),
  const BarData(15, 10),
  const BarData(16, 12),
  const BarData(17, 10),
  const BarData(18, 8),
  const BarData(19, 15),
  const BarData(20, 12),
];

Finally, add a private method called _createSampleData and return the Series list of there (this type provides the package):

static List<Series<BarData, String>> _createSampleData() {
    return [
      Series(
        id: 'Data',
        data: data,
        domainFn: (BarData data, _) => data.coins.toString(),
        measureFn: (BarData data, _) => data.monster,
      ),
    ];
}

Last but not least, add a factory that returns data and that we previously called in main.dart:

factory WitcherChart.withSampleData() => WitcherChart(_createSampleData());

With this we can create a basic chart 😎

Step. 3 – Basic bar chart

To do that is quite simple, in the witcher_chart.dart we need to return the BarChart class in build method and passed previously created :

@override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(15.0),
      child: SizedBox(
        height: 200,
        child: BarChart(
          seriesList,
          animate: true,
        ),
      ),
    );
  }

Be sure to add the seriesList variable to the top of the chart class:

class WitcherChart extends StatelessWidget {
  final List<Series<dynamic, String>> seriesList;

This should generate a simple graph with our default data.

Step. 4 – Custom styles

Now our chart looks the default, but we can go ahead and adjust the domainAxis (x-axis) and primaryMeasureAxis (y-axis) labels independently.

So let’s start with the x-axis.

In the BarChart class (it can be under the animate property) we add the domainAxis property with the following values:

domainAxis: OrdinalAxisSpec(
            tickProviderSpec: StaticOrdinalTickProviderSpec(xStaticLabels),
            renderSpec: SmallTickRendererSpec(
              labelStyle: TextStyleSpec(
                lineHeight: 3,
                fontSize: 11,
                color: Color.fromHex(code: '#4d80e4'),
              ),
              lineStyle: const LineStyleSpec(
                color: Color.transparent,
              ),
            ),
          ),

In the above code, we can manage the label text style in the labelStyle property, and we can also change the color (or hide) of the first bottom line of the chart - this is the value 0 - in the lineStyle property.

In the tickProviderSpec property we can also pass a staticTicks list where we can even change the style of each label. To do this, create a list of xStaticLabels with labels:

static final xStaticLabels = <TickSpec<String>>[
  // Possible to define style of labels directly inside
  const TickSpec(
    "1",
    label: "1 monster",
    style: TextStyleSpec(
      fontFamily: <ANY_OTHER_FONT>,
    ),
  ),
  const TickSpec(
    "10",
    label: "10 monsters",
    style: TextStyleSpec(
      fontFamily: 'thewitcher',
    ),
  ),
  TickSpec(
    "20",
    label: "20 monsters",
    style: TextStyleSpec(
      fontFamily: 'thewitcher',
      color: ColorUtil.fromDartColor(Colors.orangeAccent),
    ),
  ),
];

We can also round the corners of our bar by adding this line:

defaultRenderer: BarRendererConfig(
	cornerStrategy: const ConstCornerStrategy(<YOUR_VALUE>),
);

Okay, we can style the y-axis (primaryMeasureAxis) similarly:

primaryMeasureAxis: NumericAxisSpec(
            tickFormatterSpec: BasicNumericTickFormatterSpec(
                (num? value) => "$value \u{1F4B0}"),
            tickProviderSpec: StaticNumericTickProviderSpec(yStaticLabels),
            renderSpec: GridlineRendererSpec(
              labelStyle: TextStyleSpec(
                fontSize: 14,
                color: Color.fromHex(code: '#f8b739'),
              ),
              labelAnchor: TickLabelAnchor.after,
            ),
          ),

The difference is that tickFormatterSpec accept a function, so we can return a value from our yStaticLabels and also add e.g. Unicode (coin for The Witcher πŸ˜„) at the end using string interpolation.

As with the xStaticLabels list, create a yStaticLabels list and pass it in the StaticNumericTickProviderSpec class:

static final yStaticLabels = <TickSpec<int>>[
  // Possible to define style of labels directly inside
  const TickSpec(
    5,
    style: TextStyleSpec(
      fontFamily: <ANY_OTHER_FONT>,
    ),
  ),
  const TickSpec(
    10,
    style: TextStyleSpec(
      fontFamily: 'thewitcher',
    ),
  ),
  const TickSpec(
    15,
    style: TextStyleSpec(
      fontFamily: 'thewitcher',
    ),
  ),
  TickSpec(
    20,
    style: TextStyleSpec(
      fontFamily: 'thewitcher',
      color: ColorUtil.fromDartColor(Colors.greenAccent),
    ),
  ),
];

Step. 5 – BONUS feature

As an additional feature, we will select the lowest and highest value of the chart and change their color. It's pretty simple, just add these two lines to the _createSampleData method:

final highestValue = data.reduce((curr, next) => curr.monster > next.monster ? curr : next);
final smallestValue = data.reduce((curr, next) => curr.monster < next.monster ? curr : next);

And in the _createSampleData method in the return statement, extend the existing code with the following colorFn property:

return [
      Series(
        id: 'Data',
        data: data,
        domainFn: (BarData data, _) => data.coins.toString(),
        measureFn: (BarData data, _) => data.monster,
        colorFn: (data, _) {
          if (data.monster == highestValue.monster) {
            return Color.fromHex(code: '#2d3561');
          }
          if (data.monster == smallestValue.monster) {
            return Color.fromHex(code: '#ff0000');
          }
          return Color.fromHex(code: '#af0404');
        },
      ),
    ];

Summary πŸ‘Š

In this tutorial, I showed you how you can customize your bar chart a bit using the chart_flutter package. To learn more about this package's capabilities, check it out on pub.dev.

Thanks for reading β™₯️β™₯️

If this article was helpful, please leave a comment or πŸ‘

The full source code for this tutorial can be found here:

This is the source code of the post:
This is the source code of the post: . GitHub Gist: instantly share code, notes, and snippets.