charts_flutter の使い方

日本語の情報がびっくりするぐらいなかったので調べた。

charts_flutter

pub.dev

Material Design data visualization library written natively in Dart.

Flutterでマテリアルデザインなチャートを表示するためのライブラリ。Google謹製。 flutter_chartsという似た名前のライブラリがあるので注意。

samples

この辺にある。Galleryを見て使いたいやつを決めると良い。

google.github.io

github.com

usage

precondition

flutter --version
Flutter 1.12.13+hotfix.9 • channel stable • https://github.com/flutter/flutter.git
Framework • revision f139b11009 (3 weeks ago) • 2020-03-30 13:57:30 -0700
Engine • revision af51afceb8
Tools • Dart 2.7.2

charts_flutter: v0.9.0

サンプルコード

このサンプルコードを使って説明していく。

github.com

このサンプルでは、以下のようなシンプルなBarChartを表示している。

https://google.github.io/charts/flutter/example/bar_charts/simple_full.png

コードは以下の通り。

 class SimpleBarChart extends StatelessWidget {
   final List<charts.Series> seriesList;
   final bool animate;
 
   SimpleBarChart(this.seriesList, {this.animate});
 
   /// Creates a [BarChart] with sample data and no transition.
   factory SimpleBarChart.withSampleData() {
     return new SimpleBarChart(
       _createSampleData(),
       // Disable animations for image tests.
       animate: false,
     );
   }
 
   @override
   Widget build(BuildContext context) {
     return new charts.BarChart(
       seriesList,
       animate: animate,
     );
   }
 
   /// Create one series with sample hard coded data.
   static List<charts.Series<OrdinalSales, String>> _createSampleData() {
     final data = [
       new OrdinalSales('2014', 5),
       new OrdinalSales('2015', 25),
       new OrdinalSales('2016', 100),
       new OrdinalSales('2017', 75),
     ];
 
     return [
       new charts.Series<OrdinalSales, String>(
         id: 'Sales',
         colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault,
         domainFn: (OrdinalSales sales, _) => sales.year,
         measureFn: (OrdinalSales sales, _) => sales.sales,
         data: data,
       )
     ];
   }
 }
 
 /// Sample ordinal data type.
 class OrdinalSales {
   final String year;
   final int sales;
 
   OrdinalSales(this.year, this.sales);
 }

順を追って説明する。

まず、このクラスはStatelessWidgetを継承しており、buildメソッドでcharts.BarChartを呼んで得られるウィジェットを返している。

   @override
   Widget build(BuildContext context) {
     return new charts.BarChart(
       seriesList,
       animate: animate,
     );
   }
 

chartscharts_flutterをインポートするときのnamespaceのようなもの。 BarChartクラスはチャートウィジェットの一種。

github.com

BarChartのコンストラクタはList<common.Series<dynamic, String>> seriesListを必須とし、それ以外はOptional named parameterとして受け取る。

animate: animateは単純に表示時にアニメーションするか、といった情報を表すのでここでは深く追求しない。

seriesListList<charts.Series>オブジェクト。この例では、factoryメソッドの中身で初期化されており、 実態はstatic List<charts.Series<OrdinalSales, String>> _createSampleData()というstaticメソッドの返り値が格納されている。

charts.Seriesについては後ほど解説する。

OrdinalSalesはチャートの表示対象となる簡単なデータクラス。

/// Sample ordinal data type.
class OrdinalSales {
  final String year;
  final int sales;

  OrdinalSales(this.year, this.sales);
}

結果として、シンプルなチャートを表示するためには、List<charts.Series>を用意して、チャートウィジェットのコンストラクタに渡してやるだけで良い。

Series

さて、これでチャートが表示できるようになったが、ここでじゃあSeriesって何なのよということになる。

Seriesとは、チャートを表示するためのデータセットのことを指す。

www.highcharts.com

What is a series? A series is a set of data, for example a line graph or one set of columns. All data plotted on a chart comes from the series object. The series object has the structure:

charts_flutterでは、Seriesクラスは以下のように定義されている。

 class Series<T, D> {
   final String id;
   ...
 
   final List<T> data;
   ...
   
   /// Computed property on series.
   ///
   /// If the [index] argument is `null`, the accessor is asked to provide a
   /// property of [series] as a whole. Accessors are not required to support
   /// such usage.
   ///
   /// Otherwise, [index] must be a valid subscript into a list of `series.length`.
   typedef AccessorFn<R> = R Function(int index);
   
   typedef TypedAccessorFn<T, R> = R Function(T datum, int index);
   ...
   
   factory Series(
       {@required String id,
       @required List<T> data,
       @required TypedAccessorFn<T, D> domainFn,
       @required TypedAccessorFn<T, num> measureFn,
       ...

クラス定義を見ればわかるが、<T, D>の二つの型を取るGeneric class。

Seriesはfactoryメソッドで生成する。 引数として必要なのは、以下の通り。

  • 識別子のid
  • Seriesの生成に使うList<T> data
  • domainFn
  • measureFn

domainFnmeasureFn

これら二つはtypedefで定義されている通り、T, intを引数に取り、Rを返す関数のこと。

/// Computed property on series.
///
/// If the [index] argument is `null`, the accessor is asked to provide a
/// property of [series] as a whole. Accessors are not required to support
/// such usage.
///
/// Otherwise, [index] must be a valid subscript into a list of `series.length`.
typedef AccessorFn<R> = R Function(int index);

typedef TypedAccessorFn<T, R> = R Function(T datum, int index);

Tdatumという名前がついているので、データのこと。datumdataの単数形を意味する。 intはそのdatumのインデックスを指す。

RTypedAccessorFnの二つ目のgeneric typeで、domainFnの場合はDmeasureFnnumを指している。

つまり、 domainFnD 型を返し、 measureFnnum型を返している。

この前提を踏まえてList<charts.Series>を作っているstaticメソッドの実装を見てみる。

Seriesの生成

    /// Create one series with sample hard coded data.
    static List<charts.Series<OrdinalSales, String>> _createSampleData() {
      final data = [
        new OrdinalSales('2014', 5),
        new OrdinalSales('2015', 25),
        new OrdinalSales('2016', 100),
        new OrdinalSales('2017', 75),
      ];
  
      return [
        new charts.Series<OrdinalSales, String>(
          id: 'Sales',
          colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault,
          domainFn: (OrdinalSales sales, _) => sales.year,
          measureFn: (OrdinalSales sales, _) => sales.sales,
          data: data,
        )
      ];
    }

まず、このstaticメソッドはList<charts.Series<OrdinalSales, String>>を返す。先ほど説明した通り、SeriesはGeneric ClassでT, Dを型に取る。この例ではT: OrdinalSales, D: String

そのため、この時点でdomainFn, measureFn の型が決定する。

       @required TypedAccessorFn<OrdinalSales, String> domainFn,
       @required TypedAccessorFn<OrdinalSales, num> measureFn,

そして、Seriesの生成に使うデータを初期化している。

      final data = [
        new OrdinalSales('2014', 5),
        new OrdinalSales('2015', 25),
        new OrdinalSales('2016', 100),
        new OrdinalSales('2017', 75),
      ];

この例では直接データを用意しているが、実際のプロダクトではAPIなどから取得したデータを使用する。

このデータはList<T>であることが求められる。つまり List<OrdinalSales> となる。

そして、データを用意したらSeriesを生成する。

      return [
        new charts.Series<OrdinalSales, String>(
          id: 'Sales',
          colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault,
          domainFn: (OrdinalSales sales, _) => sales.year,
          measureFn: (OrdinalSales sales, _) => sales.sales,
          data: data,
        )
      ];

charts.Seriesのコンストラクタに id, colorFn, domainFn, measureFn, data を渡している。

さて、 domainFn, measureFn のそれぞれの関数の返り値は既に示した。以下に再掲する。

RTypedAccessorFnの二つ目のgeneric typeで、domainFnの場合はDmeasureFnnumを指している。

charts.Seriesは渡されたdata の、 data.length の長さぶんループ処理を行う。そして、 measureFn, domainFn はループ処理が走るたびに呼ばれ、 domainFn の返り値をLabel、 measureFn の返り値をValueとするデータセットを生成する。

この例を見ると、domainFnではグラフのラベルに使用するyearを、measureFnでは実際にグラフのデータとして使うsalesを返している。

それぞれ第二引数はこの例では使わないので、_で捨てている。

この第二引数も先ほど登場した。

intはそのdatumのインデックスを指す。

これは何かと言うと、 datadata.length の長さだけ domainFn, measureFn が呼ばれるときのインデックスのこと。

つまり今回は data.length == 4 なので、 0, 1, 2, 3の順に値が格納される。

使いどころとしては、例えば前回の値との差分グラフを作りたいときなどに使える。

measureFn: (OrdinalSales sales, index) => data[index].sales - data[index - 1].sales,

最終的に、Series[] でラップしたものを返している。この例では表示すべきチャートのデータセットが一つ()なので length == 1List<Series>となっている。積み重ねたい場合は、その分だけ Series を生成してやれば良い。

おわりに

Seriesの概念がちょっとハマったけど、慣れるとそこそこ便利。ラベルの色を柔軟に変えづらいのがちょっとネックかも。