【Flutter】Googleの日経平均株価っぽいグラフを作ってみた

Flutterでグラフを書く場合どんな方法があるのかなと軽く見たところfl_chartが一般的らしいので、試してみました。
今回はGoogleで日経平均株価っぽいグラフを作ることを目標にしてます。(100%一致を目指すのではなくfl_chartどんなオプションがあるのかを学ぶのが目的です。)

Googleで日経平均株価としらべるとこんなグラフが出てきます。これが目標です。

日経平均株価

Let's try!

1. 単純な折れ線グラフを描画

まずはグラフ描画に必要最低限の実装をして簡素なグラフを出してみます。

import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Googleの日経平均株価っぽいグラフ',
      home: MyChart(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Googleの日経平均株価っぽいグラフ'),
      ),
      body: const Padding(padding: EdgeInsets.all(50), child: Chart()),
    );
  }
}

class Chart extends StatelessWidget {
  const Chart({super.key});

  @override
  Widget build(BuildContext context) => LineChart( // fl_chart使用箇所
        LineChartData(
          lineBarsData: [
            LineChartBarData(
              spots: [
                FlSpot(1, 100),
                FlSpot(2, 150),
                FlSpot(3, 120),
              ],
            )
          ],
        ),
      );
}

必要最低限の実装ですが、LineChart~FlSpotまでそこそこ階層が有りますね。これを描画するとこうなります。

必要最低限の実装のみ

オプションを特に指定しない場合でも色々やってくれるんですね。

  • グラフの高さ、幅はFlSpotで与えた値の最大値、最小値にぴったりになる。

  • 目盛りは上下左右両方につく。

  • 目盛りのステップ数は自動で出してくれる。

  • グリッド線がつく。

  • 線の色はライトブルー。

2. 線の色 + 線の下のグラデーション

色が変わればそれっぽくなる気がするので、線の色と先の下のグラデーションをやってみます。(緑の部分)

class Chart extends StatelessWidget {
  const Chart({super.key});

  @override
  Widget build(BuildContext context) => LineChart(
        LineChartData(
          lineBarsData: [
            LineChartBarData(
              spots: [
                FlSpot(1, 100),
                FlSpot(2, 150),
                FlSpot(3, 120),
              ],
              color: Colors.green, // 追加
              belowBarData: BarAreaData( // 追加
                show: true,
                gradient: const LinearGradient(
                  colors: [
                    Colors.green,
                    Colors.transparent,
                    Colors.transparent,
                    Colors.transparent,
                  ],
                  begin: Alignment.topCenter,
                  end: Alignment.bottomCenter,
                ),
              ),
              dotData: FlDotData( // 追加
                show: false,
              ),
            )
          ],
        ),
      );
}

LineChartBarDataのcolor, belowBarDataを使って色をつけました。ついでにdotDataも使用して、点を消しました。

色味変更

色が変わると印象が変わりますね。

3. グラフの描画範囲変更

グラフの最大値、最小値にピッタリ合うように描画されていますが、すこし余白を持たせてあげましょう。

class Chart extends StatelessWidget {
  const Chart({super.key});

  @override
  Widget build(BuildContext context) => LineChart(
        LineChartData(
          maxY: 200, // 追加
          minY: 50, // 追加
          maxX: 10, // 追加
          minX: 1, // 追加
          lineBarsData: [
            LineChartBarData(
              spots: [
                FlSpot(1, 100),
                FlSpot(2, 150),
                FlSpot(3, 120),
              ],
              color: Colors.green,
              dotData: FlDotData(
                show: false,
              ),
              belowBarData: BarAreaData(
                show: true,
                gradient: const LinearGradient(
                  colors: [
                    Colors.green,
                    Colors.transparent,
                    Colors.transparent,
                    Colors.transparent,
                  ],
                  begin: Alignment.topCenter,
                  end: Alignment.bottomCenter,
                ),
              ),
            )
          ],
        ),
      );
}

max, minをx, yそれぞれに与えるだけです。

グラフの描画範囲変更

4. グリッド, 枠, 目盛り

グリッド線、枠、目盛りの調整を行います。

  @override
  Widget build(BuildContext context) => LineChart(
        LineChartData(
          maxY: 200,
          minY: 50,
          maxX: 10,
          minX: 1,
          lineBarsData: [/*省略*/],
          // グリッド
          gridData: FlGridData(
            drawVerticalLine: false,
            horizontalInterval: 50,
            getDrawingHorizontalLine: (value) {
              return FlLine(
                color: Colors.grey,
                strokeWidth: 0.5,
              );
            },
          ),
          // 枠
          borderData: FlBorderData(
            show: true,
            border: const Border(
              bottom: BorderSide(color: Colors.grey, width: 2.0),
            ),
          ),
          // 目盛り
          titlesData: FlTitlesData(
            rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
            topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
          ),
        ),
      );

gridData, borderData, titlesDataを使用しました。目盛りを消すためのshowTitles: falseを指定する階層が深い気がします。FlTitlesDataの引数でshowLeft: falseみたいに指定出来たほうがいいなと思います。さらにグラフの目盛りって片方にだけ表示されるものが多い気がするので初期値が上下左右全部表示なのには違和感が有ります。

グリッド、枠、目盛りの調整

5. 横軸を時刻にする(9:00 ~ 15:00)

目盛りの表記を時刻にします。その前に今のデータはx軸が時刻になっていないので少し変更しました。。

// 9時~15時で表示するためmaxX,maxY変更
maxX: 15,
minX: 9,

// x軸のデータを9時,10時,11時に変更
spots: [
 FlSpot(9, 100),
 FlSpot(10, 150),
 FlSpot(11, 120),
 ],
titlesData: FlTitlesData(
  bottomTitles: AxisTitles(
    sideTitles: SideTitles(
      getTitlesWidget: (value, meta) =>
          Text('${value.toInt().toString()}:00'),
      showTitles: true,
      interval: 1,
    ),
  ),
  rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
  topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
),

目盛りの表記はgetTitlesWidget, intervalを調整すると自由に変更できました。

x軸の目盛り調整

まとめ

データもそれっぽいものに揃えて表示してみたものがこちらになります。ここまでの作業で大体2時間ほどでした。100%一致まではまだ遠い感じですね。

日経平均株価っぽいグラフ
Googleの日経平均株価グラフ
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:intl/intl.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Googleの日経平均株価っぽいグラフ',
      home: MyChart(),
    );
  }
}

// ignore: must_be_immutable
class MyChart extends StatelessWidget {
  MyChart({Key? key}) : super(key: key);

  final DateFormat format = DateFormat('H:mm');

  final double _max = 27450;
  final double _min = 27300;

  @override
  Widget build(BuildContext context) {
    final xData = _generateXData();
    final spots = _generateSpots(xData, _max, _min);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Googleの日経平均株価っぽいグラフ'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(50),
        child: LineChart(
          LineChartData(
            maxY: _max,
            minY: _min,
            maxX: xData[xData.length - 1].toDouble(),
            minX: xData[0].toDouble(),
            lineBarsData: [
              LineChartBarData(
                spots: spots.sublist(0, 40),
                color: Colors.green,
                dotData: FlDotData(
                  show: false,
                ),
                belowBarData: BarAreaData(
                  show: true,
                  gradient: const LinearGradient(
                    colors: [
                      Color.fromARGB(67, 76, 175, 79),
                      Colors.transparent,
                      Colors.transparent,
                      Colors.transparent,
                    ],
                    begin: Alignment.topCenter,
                    end: Alignment.bottomCenter,
                  ),
                ),
              )
            ],
            borderData: FlBorderData(
              show: true,
              border: const Border(
                bottom: BorderSide(color: Colors.grey, width: 2.0),
              ),
            ),
            gridData: FlGridData(
              drawVerticalLine: false,
              horizontalInterval: 50,
              getDrawingHorizontalLine: (value) {
                return FlLine(
                  color: Colors.grey,
                  strokeWidth: 0.5,
                );
              },
            ),
            titlesData: FlTitlesData(
              leftTitles: AxisTitles(
                sideTitles: SideTitles(
                  getTitlesWidget: (value, meta) =>
                      Text(value.toInt().toString()),
                  reservedSize: 60,
                  showTitles: true,
                  interval: 50,
                ),
              ),
              bottomTitles: AxisTitles(
                sideTitles: SideTitles(
                  getTitlesWidget: (value, meta) => Text(format.format(
                      DateTime.fromMillisecondsSinceEpoch(value.toInt()))),
                  showTitles: true,
                  interval: 1000 * 60 * 60,
                ),
              ),
              rightTitles:
                  AxisTitles(sideTitles: SideTitles(showTitles: false)),
              topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
            ),
          ),
        ),
      ),
    );
  }
}

List<FlSpot> _generateSpots(List<int> xData, double max, double min) {
  // 株価の値動きっぽいデータを生成
  final t = Random();
  double currentMax = max;
  double currentMin = min;

  final List<FlSpot> spots = [];
  for (var i = 0; i < xData.length; i++) {
    spots.add(FlSpot(
      xData[i].toDouble(),
      (currentMin + (t.nextDouble() * (currentMax - currentMin)).toInt()),
    ));
    currentMax = spots[i].y + 50 > max ? max : spots[i].y + 50;
    currentMin = spots[i].y - 50 < min ? min : spots[i].y - 50;
  }
  return spots;
}

List<int> _generateXData() {
  // 5分刻みの時刻データを生成
  final now = DateTime.now();
  var tick = DateTime(now.year, now.month, now.day, 9, 0);
  final List<int> yData = [];
  while (tick.hour < 15 || tick.minute == 0) {
    yData.add(tick.millisecondsSinceEpoch);
    tick = tick.add(const Duration(minutes: 5));
  }
  return yData;
}

この記事が気に入ったらサポートをしてみませんか?