Commit 9c763f7

derdilla <derdilla06@gmail.com>
2023-05-01 08:58:05
FEAT: Graph
1 parent 505e71d
lib/components/measurement_graph.dart
@@ -1,4 +1,174 @@
+import 'dart:collection';
+
+import 'package:blood_pressure_app/model/blood_pressure.dart';
 import 'package:flutter/material.dart';
+import 'package:fl_chart/fl_chart.dart';
+import 'package:provider/provider.dart';
+import 'dart:math';
+import 'package:intl/intl.dart';
+
+class _LineChart extends StatefulWidget {
+
+  @override
+  State<StatefulWidget> createState() {
+    return _LineChartState();
+  }
+
+}
+
+class _LineChartState extends State<_LineChart> {
+  static const _recordsCount = 50;
+  var _displayMode = DisplayModes.day;
+
+  @override
+  Widget build(BuildContext context) {
+    const _pulseColor = Colors.red;
+    const _diaColor = Colors.green;
+    const _sysColor = Colors.teal;
+
+
+    return Consumer<BloodPressureModel>(
+      builder: (context, model, child) {
+        late final _dataFuture;
+        DateTime now = DateTime.now();
+        switch (_displayMode) {
+          case DisplayModes.day:
+            _dataFuture = model.getInTimeRange(DateTime(now.year, now.month, now.day), now);
+            break;
+          case DisplayModes.month:
+            _dataFuture = model.getInTimeRange(DateTime(now.year, now.month), now);
+            break;
+          case DisplayModes.year:
+            _dataFuture = model.getInTimeRange(DateTime(now.year), now);
+            break;
+          case DisplayModes.lifetime:
+            _dataFuture = model.getInTimeRange(DateTime.fromMillisecondsSinceEpoch(0), now);
+            break;
+        }
+
+
+        return FutureBuilder<UnmodifiableListView<BloodPressureRecord>>(
+          future: _dataFuture,
+          builder: (BuildContext context, AsyncSnapshot<UnmodifiableListView<BloodPressureRecord>> snapshot) {
+            Widget res;
+            switch (snapshot.connectionState) {
+              case ConnectionState.none:
+                res = const Text('not started');
+                break;
+              case ConnectionState.waiting:
+                res = const Text('loading...');
+                break;
+              default:
+                if (snapshot.hasError) {
+                  res = Text('ERROR: ${snapshot.error}');
+                } else {
+                  assert(snapshot.hasData);
+                  final data = snapshot.data ?? [];
+
+                  List<FlSpot> pulseSpots = [];
+                  List<FlSpot> diastolicSpots = [];
+                  List<FlSpot> systolicSpots = [];
+                  int pulMax = 0;
+                  int diaMax = 0;
+                  int sysMax = 0;
+                  for (var element in data) {
+                    final x = element.creationTime.millisecondsSinceEpoch.toDouble();
+                    diastolicSpots.add(FlSpot(x, element.diastolic.toDouble()));
+                    systolicSpots.add(FlSpot(x, element.systolic.toDouble()));
+                    pulseSpots.add(FlSpot(x, element.pulse.toDouble()));
+                    pulMax = max(pulMax, element.pulse);
+                    diaMax = max(diaMax, element.diastolic);
+                    sysMax = max(sysMax, element.systolic);
+                  }
+
+
+                  final noTitels = AxisTitles(sideTitles: SideTitles(reservedSize: 40, showTitles: false));
+                  res = LineChart(
+                      swapAnimationDuration: const Duration(milliseconds: 250),
+                      LineChartData(
+                          minY: 30,
+                          maxY: max(pulMax.toDouble(), max(diaMax.toDouble(), sysMax.toDouble())) + 5,
+                          titlesData: FlTitlesData(topTitles: noTitels, rightTitles:  noTitels,
+                              bottomTitles: AxisTitles(
+                                sideTitles: SideTitles(
+                                    showTitles: true,
+                                    getTitlesWidget: (double pos, TitleMeta meta) {
+                                      late final DateFormat formater;
+                                      switch (_displayMode) {
+                                        case DisplayModes.day:
+                                          formater = DateFormat('H:mm');
+                                          break;
+                                        case DisplayModes.month:
+                                          formater = DateFormat('d');
+                                          break;
+                                        case DisplayModes.year:
+                                          formater = DateFormat('MMM');
+                                          break;
+                                        case DisplayModes.lifetime:
+                                          formater = DateFormat('yyyy');
+                                      }
+                                      return Text(
+                                          formater.format(DateTime.fromMillisecondsSinceEpoch(pos.toInt()))
+                                      );
+                                    }
+                                ),
+                              )
+                          ),
+                          lineBarsData: [
+                            // high blood pressure marking acordning to https://www.texasheart.org/heart-health/heart-information-center/topics/high-blood-pressure-hypertension/
+                            LineChartBarData(
+                                spots: pulseSpots,
+                                color: _pulseColor,
+                                barWidth: 4,
+                                isCurved: true,
+                                preventCurveOverShooting: true
+                            ),
+                            LineChartBarData(
+                                spots: diastolicSpots,
+                                color: _diaColor,
+                                barWidth: 4,
+                                isCurved: true,
+                                preventCurveOverShooting: true,
+                                belowBarData: BarAreaData(
+                                    show: true,
+                                    color: Colors.red.shade400.withAlpha(100),
+                                    cutOffY: 80,
+                                    applyCutOffY: true
+                                )
+                            ),
+                            LineChartBarData(
+                                spots: systolicSpots,
+                                color: _sysColor,
+                                barWidth: 4,
+                                isCurved: true,
+                                preventCurveOverShooting: true,
+                                belowBarData: BarAreaData(
+                                    show: true,
+                                    color: Colors.red.shade400.withAlpha(100),
+                                    cutOffY: 130,
+                                    applyCutOffY: true
+                                )
+                            )
+                          ]
+                      )
+                  );
+                }
+            }
+            return res;
+          }
+      );
+      },
+    );
+  }
+
+}
+
+class DisplayModes {
+  static const day = 0;
+  static const month = 1;
+  static const year = 2;
+  static const lifetime = 3;
+}
 
 class MeasurementGraph extends StatelessWidget {
   const MeasurementGraph({super.key});
@@ -6,6 +176,12 @@ class MeasurementGraph extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     // TODO: implement build
-    return Container();
+    return Container(
+      height: 100,
+      child: Padding(
+        padding: const EdgeInsets.only(right: 16, left: 6),
+        child: _LineChart(),
+      ),
+    );
   }
 }
\ No newline at end of file
lib/components/measurement_list.dart
@@ -93,6 +93,7 @@ class MeasurementList extends StatelessWidget {
     return Container(
       child: Column (
         children: [
+          const SizedBox(height: 15 ),
           Row(
           children: [
             Expanded(
@@ -105,15 +106,18 @@ class MeasurementList extends StatelessWidget {
             ),
             Expanded(
                 flex: _tableElementsSizes[1],
-                child: Text("sys", style: TextStyle(fontWeight: FontWeight.bold))
+                child: const Text("sys",
+                    style: TextStyle(fontWeight: FontWeight.bold, color: Colors.teal))
             ),
             Expanded(
                 flex: _tableElementsSizes[2],
-                child: Text("dia", style: TextStyle(fontWeight: FontWeight.bold))
+                child: const Text("dia",
+                    style: TextStyle(fontWeight: FontWeight.bold, color: Colors.green))
             ),
             Expanded(
                 flex: _tableElementsSizes[3],
-                child: Text("pul", style: TextStyle(fontWeight: FontWeight.bold))
+                child: const Text("pul",
+                    style: TextStyle(fontWeight: FontWeight.bold, color: Colors.red))
             ),
             Expanded(
                 flex: _tableElementsSizes[4],
lib/model/blood_pressure.dart
@@ -29,9 +29,6 @@ class BloodPressureModel extends ChangeNotifier {
     return component;
   }
 
-  // All measurements (might get slow after some time)
-  UnmodifiableListView<BloodPressureRecord> get allMeasurements => UnmodifiableListView(_allMeasurements);
-
   Future<void> _cacheLast() async {
     var dbEntries = await _database.query('bloodPressureModel',
         orderBy: 'timestamp DESC', limit: _cacheCount); // descending
@@ -63,8 +60,8 @@ class BloodPressureModel extends ChangeNotifier {
     _cacheLast();
   }
 
-  /// Returns the last x BloodPressureRecords from new to old
-  /// caches new ones if necessary
+  /// Returns the last x BloodPressureRecords from new to old.
+  /// Caches new ones if necessary
   UnmodifiableListView<BloodPressureRecord> getLastX(int count) {
     List<BloodPressureRecord> lastMeasurements = [];
     
@@ -82,6 +79,26 @@ class BloodPressureModel extends ChangeNotifier {
     
     return UnmodifiableListView(lastMeasurements);
   }
+
+  /// Returns all recordings in saved in a range in ascending order
+  Future<UnmodifiableListView<BloodPressureRecord>> getInTimeRange(DateTime from, DateTime to) async {
+    var dbEntries = await _database.query('bloodPressureModel',
+      orderBy: 'timestamp DESC',
+      where: 'timestamp BETWEEN ? AND ?',
+      whereArgs: [from.millisecondsSinceEpoch, to.millisecondsSinceEpoch]
+    ); // descending
+    // syncronous part
+    List<BloodPressureRecord> recordsInRange = [];
+    for (var e in dbEntries) {
+      recordsInRange.add(BloodPressureRecord(
+          DateTime.fromMillisecondsSinceEpoch(e['timestamp']as int),
+          e['systolic'] as int,
+          e['diastolic'] as int,
+          e['pulse'] as int,
+          e['notes'] as String));
+    }
+    return UnmodifiableListView(recordsInRange);
+  }
   
 
 /* TODO:
pubspec.lock
@@ -49,6 +49,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.0.5"
+  equatable:
+    dependency: transitive
+    description:
+      name: equatable
+      sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.5"
   fake_async:
     dependency: transitive
     description:
@@ -65,6 +73,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.0.1"
+  fl_chart:
+    dependency: "direct main"
+    description:
+      name: fl_chart
+      sha256: "48a1b69be9544e2b03d9a8e843affd89e43f3194c9248776222efcb4206bb1ec"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.62.0"
   flutter:
     dependency: "direct main"
     description: flutter
pubspec.yaml
@@ -39,6 +39,7 @@ dependencies:
   # Use with the CupertinoIcons class for iOS style icons.
   cupertino_icons: ^1.0.2
   intl: ^0.18.1
+  fl_chart: ^0.62.0
 
 dev_dependencies:
   flutter_test: