リモート開発メインのソフトウェア開発企業のエンジニアブログです

FirebaseRealtimeDatabaseのJSONデータをFlutterのfl_chartでグラフ表示

前説

Raspberry piで温度湿度気圧をFirebase Realtime Databaseにデータを格納し、Flutterを使って最低限最新のデータ1つを表示するものを作成しました。

これをflutterのfl_chartでグラフ表示してみたいと思います。

https://engineering.mobalab.net/2023/05/02/firebaserealtimedatabase%E3%81%AEjson%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92flutter%E3%81%AErestapi%E3%81%A7%E8%A1%A8%E7%A4%BA/

https://engineering.mobalab.net/2023/04/26/raspberry-pi-%E3%81%A7bme280%E3%81%AE%E5%80%A4%E3%82%92realtimedatabase%E3%81%AB%E6%A0%BC%E7%B4%8D%E3%81%97%E3%81%A6%E3%81%BF%E3%81%9F/

検証環境

Windows11 22H2 Flutter 3.7.12 Dart 2.19.6 PowerShell 7

fl_chartの導入

https://pub.dev/packages/fl_chart/install

PowerShell

flutter pub add fl_chart

コードの変更

前回作成したものを変更します。

https://github.com/TKano-Null/tempviewer

main.dart

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

String _temptext = "";
List<FlSpot> _temp = const [];
List<FlSpot> _humi = const [];
List<FlSpot> _pres = const [];

Future main() async {
  await getTempData();
  runApp(const MyApp());
}

getTempData() async {
  const String tempDataUrl =
      'https://xxxxxx.firebaseio.com/users/xxxxxx/tempdata.json?orderBy="key"&limitToLast=10';
  final response = await Dio().get(tempDataUrl);
  if (response.statusCode == 200) {
    String rawJson = response.toString();
    _temp = [];
    _humi = [];
    _pres = [];
    int index = 0;
    Map<String, dynamic> map = jsonDecode(rawJson);
    map.forEach((key, value) {
      final DateTime date =
          DateTime.fromMillisecondsSinceEpoch((value['date'] * 1000).round());
      final hour = date.hour;
      final minute = date.minute;
      final String timeText = '$hour' '時' '$minute' '分';
      final String temp = value['temp'].toStringAsFixed(1);
      final String tempText = '$temp' '℃';
      final String humi = value['humi'].toStringAsFixed(1);
      final String humiText = '$humi' '%';
      final String pres = (value['pres'] / 100).toStringAsFixed(1);
      final String presText = '$pres' 'hPa';
      _temptext =
          '$timeText' '\n' '$tempText' '\n' '$humiText' '\n' '$presText';
      //final String gTimeText = '$hour' '$minute';
      index++;
      _temp.add(FlSpot(index.toDouble(), double.parse(temp)));
      _humi.add(FlSpot(index.toDouble(), double.parse(humi)));
      _pres.add(FlSpot(index.toDouble(), double.parse(pres)));
    });
    print(map);
  } else {
    print(response.statusCode);
  }
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  //int _counter = 0;

  //void _incrementCounter() {
  void _tempDataController() async {
    await getTempData();
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [
            Text(_temptext),
            Container(
              height: 400,
              width: 400,
              child: LineChart(
                LineChartData(lineBarsData: [
                  LineChartBarData(isCurved: true, spots: _temp)
                ]),
              ),
            ),
            Container(
              height: 400,
              width: 400,
              child: LineChart(
                LineChartData(lineBarsData: [
                  LineChartBarData(isCurved: true, spots: _humi)
                ]),
              ),
            ),
            Container(
              height: 400,
              width: 400,
              child: LineChart(
                LineChartData(lineBarsData: [
                  LineChartBarData(isCurved: true, spots: _pres)
                ], minY: 990, maxY: 1010),
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _tempDataController,
        child: const Icon(Icons.autorenew),
      ),
    );
  }
}

import

まずfl_chartをインポートします。

import 'package:fl_chart/fl_chart.dart';

FLSpotの作成

FLSpotは最終的にグラフの1点になるx,y座標を指定します。このFLSpotを配列で持たせることで線を表現します。

      _temp.add(FlSpot(index.toDouble(), double.parse(temp)));
      _humi.add(FlSpot(index.toDouble(), double.parse(humi)));
      _pres.add(FlSpot(index.toDouble(), double.parse(pres)));

グラフ表示

実際にwidgetへのせていきます。

              child: LineChart(
                LineChartData(lineBarsData: [
                  LineChartBarData(isCurved: true, spots: _temp)
                ]),
              ),

LineChart,LineChartData,LineChartBarDataには必須オプションと任意オプションが指定できます。

任意オプションを使用して線の色等の見た目の変更ができますが、今回は最小量の指定をしています。

isCurvedオプションは線を曲線にします。

LineChartDataに必須オプションのlineBarsDataにLineChartBarDataの配列を指定しています。

今回は配列の中身が1つだけですが、2つ以上にすることで同じグラフ内に複数の線を表示出来ます。

LineCartBarDataに必須オプションのspotsに_temp(FLSpotの配列)を指定し、完了です。

課題

グラフのX軸の単位を時間にしたかったのですが、今回は順序のみを指定しています。

これは横軸のTitleを変更することで順序を保ったまま見た目上の単位を時間に変更できるようです。

しかし、これを行うために多数のオプションを指定するなど工数がかかってしまうようなので今回は断念しました。

詰まった点

1つのグラフ内に温度、湿度を同時に表示したかったのですが、データの単位が異なるとうまく表示できず、値を%にしてY軸のTitleのLeft,Rightに適切に表示用データを用意するなど対応が必要なようです。

感想

fl_chartは比較的簡単にグラフ表示を実装できましたが、様々なオプションがあり自由度が高い分、理想とするようなグラフにするには学習コストが必要になる印象でした。

← 前の投稿

MSYS2 で Windows 上に *NIX ぽい環境を構築する

次の投稿 →

[Rails] Active Admin で純粋な webpack を使用する際に行ったこと

コメントを残す