0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Flutter & fl_chart】csvをグラフ表示する方法

Last updated at Posted at 2024-10-09

背景

現在,ユーザに合わせて様々なOSに対応するアプリ開発をしようとする際に,クロスプラットフォーム開発環境を用いるのが一般的となっています.
本記事は,Flutterを用いた開発でcsvの内容をグラフ表示した内容をまとめています.

アプリ概要

概要

ユーザが選択したcsvファイル(時間と速度がまとめられている)を読み込み,そのグラフを表示するアプリ.

画面設計

画面上部にグラフ表示(csv読み込み前は何も表示しない)
画面下部右に「csv読み込み」ボタン

操作

「csv読み込み」ボタンを押下すると,ローカルに保存されているcsvファイルを選択できる.(file_pickerライブラリを使用する)
csvファイルを選択すると,そのcsvファイルを読み込む.(File クラスを使用する)
csvファイルの内容を解析し,グラフに表示する.(fl_chartライブラリを使用する)
グラフは横軸を時間(s),縦軸を速度(m/min)とする.

csvフォーマット

No.:5ms周期のカウンタ
Speed:速度(cm/min)

No. Speed
1 0
2 1
3 2
... ...

ソースコード

csv読み込みとグラフ描写の処理を分けて記載しています.
若干コメント入れています.

pubspec.yaml
// 以下を付け足し
dependencies:
  flutter:
    sdk: flutter
  file_picker: ^8.1.2
  fl_chart: ^0.69.0
main.dart
import 'dart:async';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(CsvGraphApp());
}

class CsvGraphApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'CSV Graph Viewer',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: CsvGraphPage(),
    );
  }
}

class CsvGraphPage extends StatefulWidget {
  @override
  _CsvGraphPageState createState() => _CsvGraphPageState();
}

class _CsvGraphPageState extends State<CsvGraphPage> {
  List<FlSpot> _dataPoints = [];

  Future<void> _pickAndLoadCsv() async {
    // ファイルピッカーを開いてCSVファイルを選択
    FilePickerResult? result = await FilePicker.platform.pickFiles(
      type: FileType.custom,
      allowedExtensions: ['csv'],
    );

    if (result != null && result.files.single.path != null) {
      File file = File(result.files.single.path!);
      String content = await file.readAsString();
      _parseCsv(content);
    } else {
      // ユーザーがファイル選択をキャンセルした場合
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('ファイルの選択がキャンセルされました。')),
      );
    }
  }

  // csvの読み込み
  void _parseCsv(String content) {
    List<FlSpot> tempData = [];
    List<String> lines = content.split('\n');

    // 2行目以降をパース
    for (int i = 1; i < lines.length; i++) {
      String line = lines[i].trim();
      if (line.isEmpty) continue; // 空行をスキップ

      List<String> parts = line.split(',');
      if (parts.length < 2) continue; // 不完全な行をスキップ

      try {
        int no = int.parse(parts[0].trim());
        double speedCm = double.parse(parts[1].trim());

        // cm/min を m/min に変換
        double speed = speedCm / 100.0;

        // 時間を秒単位に変換(No. * 5ms)
        double timeInSeconds = (no * 5) / 1000.0;

        tempData.add(FlSpot(timeInSeconds, speed));
      } catch (e) {
        // パースエラーが発生した場合はスキップ
        continue;
      }
    }

    setState(() {
      _dataPoints = tempData;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('CSVグラフ表示'),
      ),
      body: Column(
        children: [
          Expanded(
            child: _dataPoints.isEmpty
                ? Center(child: Text('CSVファイルを読み込んでください。'))
                : Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: LineChart(
                      LineChartData(
                        gridData: FlGridData(show: true),
                        titlesData: FlTitlesData(
                          // 上部のタイトルを非表示
                          topTitles: AxisTitles(
                            sideTitles: SideTitles(showTitles: false),
                          ),
                          // 右側のタイトルを非表示
                          rightTitles: AxisTitles(
                            sideTitles: SideTitles(showTitles: false),
                          ),
                          // 下部のタイトルと単位の設定
                          bottomTitles: AxisTitles(
                            sideTitles: SideTitles(
                              showTitles: true,
                              reservedSize: 30,
                              interval: _calculateXInterval(),
                              getTitlesWidget: (value, meta) {
                                // 値のみ表示
                                return Padding(
                                  padding: const EdgeInsets.only(top: 8.0),
                                  child: Text('${value.toStringAsFixed(2)}'),
                                );
                              },
                            ),
                            axisNameWidget: Padding(
                              padding: const EdgeInsets.only(top: 8.0),
                              child: Text('[s]'),
                            ),
                            axisNameSize: 20,
                          ),
                          // 左側のタイトルと単位の設定
                          leftTitles: AxisTitles(
                            sideTitles: SideTitles(
                              showTitles: true,
                              reservedSize: 40,
                              interval: _calculateYInterval(),
                              getTitlesWidget: (value, meta) {
                                // 値のみ表示
                                return Text('${value.toStringAsFixed(1)}');
                              },
                            ),
                            axisNameWidget: Padding(
                              padding: const EdgeInsets.only(right: 8.0),
                              child: Text('[m/min]'),
                            ),
                            axisNameSize: 20,
                          ),
                        ),
                        borderData: FlBorderData(
                          show: true,
                          border: Border.all(color: Colors.black, width: 1),
                        ),
                        minX: _dataPoints.first.x,
                        maxX: _dataPoints.last.x,
                        minY: _getMinY(),
                        maxY: _getMaxY(),
                        lineBarsData: [
                          LineChartBarData(
                            spots: _dataPoints,
                            isCurved: false,
                            color: Colors.blue,
                            barWidth: 2,
                            dotData: FlDotData(show: false),
                          ),
                        ],
                      ),
                    ),
                  ),
          ),
          SizedBox(height: 20),
          Align(
            alignment: Alignment.bottomRight,
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: ElevatedButton.icon(
                onPressed: _pickAndLoadCsv,
                icon: Icon(Icons.file_upload),
                label: Text('csv読み込み'),
              ),
            ),
          ),
        ],
      ),
    );
  }

  double _getMinY() {
    double minY = _dataPoints.map((e) => e.y).reduce((a, b) => a < b ? a : b);
    return minY - 1; // グラフが見やすいようにマージンを追加
  }

  double _getMaxY() {
    double maxY = _dataPoints.map((e) => e.y).reduce((a, b) => a > b ? a : b);
    return maxY + 1; // グラフが見やすいようにマージンを追加
  }

  double _calculateXInterval() {
    double totalTime = _dataPoints.last.x - _dataPoints.first.x;
    return totalTime / 5; // 適当な間隔を設定
  }

  double _calculateYInterval() {
    double maxY = _getMaxY();
    return (maxY / 5).ceilToDouble();
  }
}

動作確認

macOS,iOS(エミュレータ),Android(エミュレータ),chromeで動作確認をしました.
こちらの記事を参考にエミュレータを起動しました.

代表として,macOSで動作させた画像を貼っておきます.

スクリーンショット 2024-09-29 21.04.46.png

スクリーンショット 2024-09-29 21.05.09.png

スクリーンショット 2024-09-29 21.05.22.png

おわりに

1つのソースコードから多くの環境で動作する実行ファイルが作成できるのは便利でした.
chromeで動作させたときに,セキュリティの問題でローカルのcsvが読み込めなかったので,実行環境による動作の差異に注意が必要だと感じました.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?