Commit c03409d

derdilla <derdilla06@gmail.com>
2023-05-14 14:50:08
FEAT: add Time-Resolved statistics
1 parent 44c3a32
Changed files (2)
lib/model/blood_pressure.dart
@@ -1,4 +1,4 @@
-import 'dart:collection';
+import 'package:collection/collection.dart';
 import 'dart:io';
 import 'package:csv/csv.dart';
 import 'package:file_picker/file_picker.dart';
@@ -148,6 +148,63 @@ class BloodPressureModel extends ChangeNotifier {
     return val ?? -1;
   }
 
+  /// outer list is type (0 -> diastolic, 1 -> systolic, 2 -> pulse)
+  /// inner list index is hour of day ([0] -> 00:00-00:59; [1] -> ...)
+  Future<List<List<int>>> getAllAvgsRelativeToDaytime({bool interpolate = false}) async {
+    // setup vars
+    List<List<int>> allDiaValuesRelativeToTime = [[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]];
+    List<List<int>> allSysValuesRelativeToTime = [[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]];
+    List<List<int>> allPulValuesRelativeToTime = [[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]];
+
+    // sort all data
+    final dbRes = await _database.query('bloodPressureModel', columns: ['*']);
+    for (var entry in dbRes) {
+      DateTime ts = DateTime.fromMillisecondsSinceEpoch(entry['timestamp'] as int);
+      allDiaValuesRelativeToTime[ts.hour].add(entry['diastolic'] as int);
+      allSysValuesRelativeToTime[ts.hour].add(entry['systolic'] as int);
+      allPulValuesRelativeToTime[ts.hour].add(entry['pulse'] as int);
+    }
+    for(int i = 0; i < 24; i++) { // TODO: interpolate for every day instead without using allow of resources
+      if (allDiaValuesRelativeToTime[i].isEmpty) { // fixme next might be empty
+        allDiaValuesRelativeToTime[i].add(0);
+      }
+      if (allSysValuesRelativeToTime[i].isEmpty) {
+        allSysValuesRelativeToTime[i].add(0);
+      }
+      if (allPulValuesRelativeToTime[i].isEmpty) {
+        allPulValuesRelativeToTime[i].add(0);
+      }
+    }
+
+    if (interpolate) {
+      for(int i = 0; i < 24; i++) {
+        var prev = (i - 1 >= 0) ? (i - 1) : 23;
+        var next = (i + 1 <= 23) ? (i + 1) : 0;
+        allDiaValuesRelativeToTime[i].add([
+          allDiaValuesRelativeToTime[prev].average,
+          allDiaValuesRelativeToTime[next].average
+        ].average.toInt());
+        allSysValuesRelativeToTime[i].add([
+          allSysValuesRelativeToTime[prev].average,
+          allSysValuesRelativeToTime[next].average
+        ].average.toInt());
+        allPulValuesRelativeToTime[i].add([
+          allPulValuesRelativeToTime[prev].average,
+          allPulValuesRelativeToTime[next].average
+        ].average.toInt());
+      }
+    }
+
+    // make avgs
+    List<List<int>> res = [[],[],[]];
+    for(int i = 0; i < 24; i++) {
+      res[0].add(allDiaValuesRelativeToTime[i].average.toInt());
+      res[1].add(allSysValuesRelativeToTime[i].average.toInt());
+      res[2].add(allPulValuesRelativeToTime[i].average.toInt());
+    }
+    return res;
+  }
+
   Future<void> save(void Function(bool success, String? msg) callback, {bool exportAsText = false}) async {
     // create csv
     String csvData = 'timestampUnixMs, systolic, diastolic, pulse, notes\n';
lib/screens/statistics.dart
@@ -1,4 +1,5 @@
 import 'package:blood_pressure_app/model/blood_pressure.dart';
+import 'package:blood_pressure_app/model/settings.dart';
 import 'package:fl_chart/fl_chart.dart';
 import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
@@ -16,25 +17,123 @@ class StatisticsPage extends StatelessWidget {
       body: SingleChildScrollView(
         child: Consumer<BloodPressureModel>(
           builder: (context, model, child) {
-            return Wrap(
-              children: [
-                Statistic(
-                    caption: const Text('Measurement count'),
-                    child: futureInt(model.count)
-                ),
-                Statistic(
-                    caption: const Text('Diastolic avg.'),
-                    child: futureInt(model.avgDia)
-                ),
-                Statistic(
-                    caption: const Text('Systolic avg.'),
-                    child: futureInt(model.avgSys)
-                ),
-                Statistic(
-                    caption: const Text('Pulse avg.'),
-                    child: futureInt(model.avgPul)
-                ),
-              ],
+            return Consumer<Settings>(
+              builder: (context, settings, child) {
+                return Column(
+                  children: [
+                    Statistic(
+                        caption: const Text('Measurement count'),
+                        child: futureInt(model.count)
+                    ),
+                    // Averages
+                    Row(
+                      children: [
+                        const Spacer(),
+                        Statistic(
+                          caption: Text('Diastolic avg.',
+                            style: TextStyle(color: settings.diaColor, fontWeight: FontWeight.w700),
+                          ),
+                          smallEdges: true,
+                          child: futureInt(model.avgDia),
+                        ),
+                        const Spacer(),
+                        Statistic(
+                          caption: Text('Systolic avg.',
+                            style: TextStyle(color: settings.sysColor, fontWeight: FontWeight.w700),),
+                          smallEdges: true,
+                          child: futureInt(model.avgSys),
+                        ),
+                        const Spacer(),
+                        Statistic(
+                          caption: Text('Pulse avg.',
+                            style: TextStyle(color: settings.pulColor, fontWeight: FontWeight.w700),),
+                          smallEdges: true,
+                          child: futureInt(model.avgPul),
+                        ),
+                        const Spacer(),
+                      ],
+                    ),
+                    Statistic(
+                      caption: const Text('Time-Resolved Metrics'),
+                      child: FutureBuilder<List<List<int>>>(
+                          future: model.getAllAvgsRelativeToDaytime(
+                              interpolate: true),
+                          builder: (BuildContext context, AsyncSnapshot<List<
+                              List<int>>> snapshot) {
+                            switch (snapshot.connectionState) {
+                              case ConnectionState.none:
+                                return const Text('not started');
+                              case ConnectionState.waiting:
+                                return const Text('loading...');
+                              default:
+                                if (snapshot.hasError) {
+                                  return Text('ERROR: ${snapshot.error}');
+                                }
+                                assert(snapshot.hasData);
+                                assert(snapshot.data != null);
+                                final daytimeAvgs = snapshot.data ?? [];
+                                const opacity = 0.5;
+                                return SizedBox(
+                                  width: 500,
+                                  height: 270,
+                                  child: RadarChart(
+                                    RadarChartData(
+                                      radarShape: RadarShape.circle,
+                                      radarBorderData: const BorderSide(color: Colors.transparent),
+                                      gridBorderData: BorderSide(
+                                          color: Theme.of(context).dividerColor,
+                                          width: 2
+                                      ),
+                                      tickBorderData: BorderSide(
+                                          color: Theme.of(context).dividerColor,
+                                          width: 2),
+                                      ticksTextStyle: const TextStyle(color: Colors.transparent),
+                                      tickCount: 5,
+                                      titleTextStyle: const TextStyle(
+                                        fontSize: 25
+                                      ),
+                                      getTitle: (pos, value) {
+                                        if (pos % 2 == 0) {
+                                          return RadarChartTitle(
+                                              text: '$pos',
+                                              positionPercentageOffset: 0.05
+                                          );
+                                        }
+                                        return const RadarChartTitle(text: '');
+                                      },
+                                      dataSets: [
+                                        RadarDataSet(
+                                            dataEntries: intListToRadarEntry(daytimeAvgs[0]),
+                                            borderColor: settings.diaColor,
+                                            fillColor: settings.diaColor.withOpacity(opacity),
+                                            entryRadius: 0,
+                                            borderWidth: 3
+                                        ),
+                                        RadarDataSet(
+                                            dataEntries: intListToRadarEntry(daytimeAvgs[1]),
+                                            borderColor: settings.sysColor,
+                                            fillColor: settings.sysColor.withOpacity(opacity),
+                                            entryRadius: 0,
+                                            borderWidth: 3
+                                        ),
+                                        RadarDataSet(
+                                            dataEntries: intListToRadarEntry(daytimeAvgs[2]),
+                                            borderColor: settings.pulColor,
+                                            fillColor: settings.pulColor.withOpacity(opacity),
+                                            entryRadius: 0,
+                                            borderWidth: 3
+                                        ),
+                                      ],
+                                    ),
+                                  ),
+                                );
+                            }
+                          }
+                      ),
+                    ),
+                  ],
+                );
+              }
             );
           },
         )
@@ -64,21 +163,37 @@ class StatisticsPage extends StatelessWidget {
         }
     );
   }
+
+  List<RadarEntry> intListToRadarEntry(List<int> data) {
+    var res = <RadarEntry>[];
+    for (var v in data) {
+      res.add(RadarEntry(value: v.toDouble()));
+    }
+    return res;
+  }
 }
 
 class Statistic extends StatelessWidget {
   final Widget caption;
   final Widget child;
+  final bool smallEdges;
 
-  const Statistic({super.key, required this.caption, required this.child, });
+  const Statistic({super.key, required this.caption, required this.child, this.smallEdges=false});
 
   @override
-  Widget build(BuildContext context) { // TODO
+  Widget build(BuildContext context) {
+    double sides = 20;
+    double top = 20;
+    double padding = 30;
+    if (smallEdges) {
+      sides = 0;
+      padding = 10;
+    }
     return Container(
-      margin: const EdgeInsets.only(left:20, right: 20, top: 20),
+      margin: EdgeInsets.only(left: sides, right: sides, top: top),
       constraints: const BoxConstraints(
           minHeight: 50,
-          minWidth: 150
+          minWidth: 110
       ),
       decoration: BoxDecoration(
         border: Border.all(
@@ -98,7 +213,7 @@ class Statistic extends StatelessWidget {
             ),
           ),
           Container(
-            padding: const EdgeInsets.all(30),
+            padding: EdgeInsets.only(left: padding, right: padding, bottom: padding, top: padding+5),
             child: Align(
               alignment: Alignment.center,
               child: DefaultTextStyle(