Commit 7f35b00

derdilla <derdilla06@gmail.com>
2023-07-16 08:43:10
add time intervall selection to statistics page
1 parent 7ac2bfb
lib/model/blood_pressure_analyzer.dart
@@ -10,34 +10,42 @@ class BloodPressureAnalyser {
 
   int get count => _records.length;
 
-  int get avgDia => _nonNullDia.reduce((a, b) => a + b) ~/ _nonNullDia.length;
+  int get avgDia => _safeResult(() => _nonNullDia.average.toInt(), (r) => r.diastolic);
 
-  int get avgPul => _nonNullPul.reduce((a, b) => a + b) ~/ _nonNullPul.length;
+  int get avgPul => _safeResult(() => _nonNullPul.average.toInt(), (r) => r.pulse);
 
-  int get avgSys => _nonNullSys.reduce((a, b) => a + b) ~/ _nonNullSys.length;
+  int get avgSys => _safeResult(() => _nonNullSys.average.toInt(), (r) => r.systolic);
 
-  int get maxDia => _nonNullDia.reduce(max);
+  int get maxDia => _safeResult(() => _nonNullDia.reduce(max), (r) => r.diastolic);
 
-  int get maxPul => _nonNullPul.reduce(max);
+  int get maxPul => _safeResult(() => _nonNullPul.reduce(max), (r) => r.pulse);
 
-  int get maxSys => _nonNullSys.reduce(max);
+  int get maxSys => _safeResult(() => _nonNullSys.reduce(max), (r) => r.systolic);
 
-  int get minDia => _nonNullDia.reduce(min);
+  int get minDia => _safeResult(() => _nonNullDia.reduce(min), (r) => r.diastolic);
 
-  int get minPul => _nonNullPul.reduce(min);
+  int get minPul => _safeResult(() =>  _nonNullPul.reduce(min), (r) => r.pulse);
 
-  int get minSys => _nonNullSys.reduce(min);
+  int get minSys => _safeResult(() => _nonNullSys.reduce(min), (r) => r.systolic);
 
-  DateTime get firstDay {
+  //TODO make first and last day nullable
+  DateTime? get firstDay {
+    if (_records.isEmpty) return null;
     _records.sort((a, b) => a.creationTime.compareTo(b.creationTime));
     return _records.first.creationTime;
   }
 
-  DateTime get lastDay {
+  DateTime? get lastDay {
+    if (_records.isEmpty) return null;
     _records.sort((a, b) => a.creationTime.compareTo(b.creationTime));
     return _records.last.creationTime;
   }
 
+  int _safeResult(int Function() f, int? Function(BloodPressureRecord) lengthOneResult) {
+    if (_records.isEmpty) return -1;
+    if (_records.length == 1) return lengthOneResult(_records.first) ?? -1;
+    return f();
+  }
   Iterable<int> get _nonNullDia => _records.where((e) => e.diastolic!=null).map<int>((e) => e.diastolic!);
   Iterable<int> get _nonNullSys => _records.where((e) => e.systolic!=null).map<int>((e) => e.systolic!);
   Iterable<int> get _nonNullPul => _records.where((e) => e.pulse!=null).map<int>((e) => e.pulse!);
@@ -48,6 +56,7 @@ class BloodPressureAnalyser {
 
     final firstDay = this.firstDay;
     final lastDay = this.lastDay;
+    if (firstDay == null || lastDay == null) return -1;
 
     if (firstDay.millisecondsSinceEpoch == -1 || lastDay.millisecondsSinceEpoch == -1) {
       return -1;
lib/screens/subsettings/export_import_screen.dart
@@ -132,7 +132,11 @@ class ExportDataRangeSettings extends StatelessWidget {
               var model = Provider.of<BloodPressureModel>(context, listen: false);
               var analyzer = BloodPressureAnalyser(await model.all);
               if(!context.mounted) return;
-              var newRange = await showDateRangePicker(context: context, firstDate: analyzer.firstDay, lastDate: analyzer.lastDay);
+              var newRange = await showDateRangePicker(
+                  context: context,
+                  firstDate: analyzer.firstDay??DateTime.fromMillisecondsSinceEpoch(0),
+                  lastDate: analyzer.lastDay??DateTime.now()
+              );
               if (newRange == null && context.mounted) {
                 ScaffoldMessenger.of(context)
                     .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.errNoRangeForExport)));
lib/screens/statistics.dart
@@ -1,6 +1,7 @@
 import 'dart:collection';
 
 import 'package:blood_pressure_app/components/consistent_future_builder.dart';
+import 'package:blood_pressure_app/components/display_interval_picker.dart';
 import 'package:blood_pressure_app/model/blood_pressure.dart';
 import 'package:blood_pressure_app/model/blood_pressure_analyzer.dart';
 import 'package:blood_pressure_app/model/settings_store.dart';
@@ -20,129 +21,136 @@ class StatisticsPage extends StatelessWidget {
         title: Text(AppLocalizations.of(context)!.statistics),
         backgroundColor: Theme.of(context).primaryColor,
       ),
-      body: SingleChildScrollView(child: Consumer<BloodPressureModel>(
-        builder: (context, model, child) {
-          return ConsistentFutureBuilder<UnmodifiableListView<BloodPressureRecord>>(
-              future: model.all, 
-              onData: (context, data) {
-                return Consumer<Settings>(builder: (context, settings, child) {
-                  final analyzer = BloodPressureAnalyser(data);
-                  return Column(
-                    children: [
-                      Statistic(
-                          key: const Key('measurementCount'),
-                          caption: Text(AppLocalizations.of(context)!.measurementCount), child: displayInt(analyzer.count)),
-                      // special measurements
-                      StatisticsRow(
-                        caption1: Text(
-                          AppLocalizations.of(context)!.avgOf(AppLocalizations.of(context)!.sysLong),
-                          style: TextStyle(color: settings.sysColor, fontWeight: FontWeight.w700),
+      body: Container(
+        margin: const EdgeInsets.only(bottom: 100),
+        child: SingleChildScrollView(child: Consumer<BloodPressureModel>(
+          builder: (context, model, child) {
+            return Consumer<Settings>(builder: (context, settings, child) {
+              return ConsistentFutureBuilder<UnmodifiableListView<BloodPressureRecord>>(
+                  future: model.getInTimeRange(settings.displayDataStart, settings.displayDataEnd),
+                  onData: (context, data) {
+                    final analyzer = BloodPressureAnalyser(data.toList());
+                    return Column(
+                      children: [
+                        Statistic(
+                            key: const Key('measurementCount'),
+                            caption: Text(AppLocalizations.of(context)!.measurementCount), child: displayInt(analyzer.count)),
+                        // special measurements
+                        StatisticsRow(
+                          caption1: Text(
+                            AppLocalizations.of(context)!.avgOf(AppLocalizations.of(context)!.sysLong),
+                            style: TextStyle(color: settings.sysColor, fontWeight: FontWeight.w700),
+                          ),
+                          child1: displayInt(analyzer.avgSys),
+                          caption2: Text(
+                            AppLocalizations.of(context)!.avgOf(AppLocalizations.of(context)!.diaLong),
+                            style: TextStyle(color: settings.diaColor, fontWeight: FontWeight.w700),
+                          ),
+                          child2: displayInt(analyzer.avgDia),
+                          caption3: Text(
+                            AppLocalizations.of(context)!.avgOf(AppLocalizations.of(context)!.pulLong),
+                            style: TextStyle(color: settings.pulColor, fontWeight: FontWeight.w700),
+                          ),
+                          child3: displayInt(analyzer.avgPul),
                         ),
-                        child1: displayInt(analyzer.avgSys),
-                        caption2: Text(
-                          AppLocalizations.of(context)!.avgOf(AppLocalizations.of(context)!.diaLong),
-                          style: TextStyle(color: settings.diaColor, fontWeight: FontWeight.w700),
-                        ),
-                        child2: displayInt(analyzer.avgDia),
-                        caption3: Text(
-                          AppLocalizations.of(context)!.avgOf(AppLocalizations.of(context)!.pulLong),
-                          style: TextStyle(color: settings.pulColor, fontWeight: FontWeight.w700),
-                        ),
-                        child3: displayInt(analyzer.avgPul),
-                      ),
-                      Statistic(
+                        Statistic(
                           caption: Text(AppLocalizations.of(context)!.measurementsPerDay),
                           child: displayInt(analyzer.measurementsPerDay)),
-                      StatisticsRow(
-                        caption1: Text(
-                          AppLocalizations.of(context)!.minOf(AppLocalizations.of(context)!.sysLong),
-                          style: TextStyle(color: settings.sysColor, fontWeight: FontWeight.w700),
-                        ),
-                        child1: displayInt(analyzer.minSys),
-                        caption2: Text(
-                          AppLocalizations.of(context)!.minOf(AppLocalizations.of(context)!.diaLong),
-                          style: TextStyle(color: settings.diaColor, fontWeight: FontWeight.w700),
-                        ),
-                        child2: displayInt(analyzer.minDia),
-                        caption3: Text(
-                          AppLocalizations.of(context)!.minOf(AppLocalizations.of(context)!.pulLong),
-                          style: TextStyle(color: settings.pulColor, fontWeight: FontWeight.w700),
-                        ),
-                        child3: displayInt(analyzer.minPul),
-                      ),
-                      StatisticsRow(
-                        caption2: Text(
-                          AppLocalizations.of(context)!.maxOf(AppLocalizations.of(context)!.diaLong),
-                          style: TextStyle(color: settings.diaColor, fontWeight: FontWeight.w700),
+                        StatisticsRow(
+                          caption1: Text(
+                            AppLocalizations.of(context)!.minOf(AppLocalizations.of(context)!.sysLong),
+                            style: TextStyle(color: settings.sysColor, fontWeight: FontWeight.w700),
+                          ),
+                          child1: displayInt(analyzer.minSys),
+                          caption2: Text(
+                            AppLocalizations.of(context)!.minOf(AppLocalizations.of(context)!.diaLong),
+                            style: TextStyle(color: settings.diaColor, fontWeight: FontWeight.w700),
+                          ),
+                          child2: displayInt(analyzer.minDia),
+                          caption3: Text(
+                            AppLocalizations.of(context)!.minOf(AppLocalizations.of(context)!.pulLong),
+                            style: TextStyle(color: settings.pulColor, fontWeight: FontWeight.w700),
+                          ),
+                          child3: displayInt(analyzer.minPul),
                         ),
-                        child2: displayInt(analyzer.maxDia),
-                        caption1: Text(
-                          AppLocalizations.of(context)!.maxOf(AppLocalizations.of(context)!.sysLong),
-                          style: TextStyle(color: settings.sysColor, fontWeight: FontWeight.w700),
+                        StatisticsRow(
+                          caption2: Text(
+                            AppLocalizations.of(context)!.maxOf(AppLocalizations.of(context)!.diaLong),
+                            style: TextStyle(color: settings.diaColor, fontWeight: FontWeight.w700),
+                          ),
+                          child2: displayInt(analyzer.maxDia),
+                          caption1: Text(
+                            AppLocalizations.of(context)!.maxOf(AppLocalizations.of(context)!.sysLong),
+                            style: TextStyle(color: settings.sysColor, fontWeight: FontWeight.w700),
+                          ),
+                          child1: displayInt(analyzer.maxSys),
+                          caption3: Text(
+                            AppLocalizations.of(context)!.maxOf(AppLocalizations.of(context)!.pulLong),
+                            style: TextStyle(color: settings.pulColor, fontWeight: FontWeight.w700),
+                          ),
+                          child3: displayInt(analyzer.maxPul),
                         ),
-                        child1: displayInt(analyzer.maxSys),
-                        caption3: Text(
-                          AppLocalizations.of(context)!.maxOf(AppLocalizations.of(context)!.pulLong),
-                          style: TextStyle(color: settings.pulColor, fontWeight: FontWeight.w700),
-                        ),
-                        child3: displayInt(analyzer.maxPul),
-                      ),
-                      // Time-Resolved Metrics
-                      Statistic(
-                        caption: Text(AppLocalizations.of(context)!.timeResolvedMetrics),
-                        child: (() {
-                          final data = analyzer.allAvgsRelativeToDaytime;
-                          const opacity = 0.5;
-                          return SizedBox(
-                            width: 500,
-                            height: 500,
-                            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(data[0]),
-                                      borderColor: settings.diaColor,
-                                      fillColor: settings.diaColor.withOpacity(opacity),
-                                      entryRadius: 0,
-                                      borderWidth: settings.graphLineThickness),
-                                  RadarDataSet(
-                                      dataEntries: intListToRadarEntry(data[1]),
-                                      borderColor: settings.sysColor,
-                                      fillColor: settings.sysColor.withOpacity(opacity),
-                                      entryRadius: 0,
-                                      borderWidth: settings.graphLineThickness),
-                                  RadarDataSet(
-                                      dataEntries: intListToRadarEntry(data[2]),
-                                      borderColor: settings.pulColor,
-                                      fillColor: settings.pulColor.withOpacity(opacity),
-                                      entryRadius: 0,
-                                      borderWidth: settings.graphLineThickness),
-                                ],
+                        // Time-Resolved Metrics
+                        Statistic(
+                          caption: Text(AppLocalizations.of(context)!.timeResolvedMetrics),
+                          child: (() {
+                            final data = analyzer.allAvgsRelativeToDaytime;
+                            const opacity = 0.5;
+                            return SizedBox(
+                              width: 500,
+                              height: 500,
+                              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(data[0]),
+                                        borderColor: settings.diaColor,
+                                        fillColor: settings.diaColor.withOpacity(opacity),
+                                        entryRadius: 0,
+                                        borderWidth: settings.graphLineThickness),
+                                    RadarDataSet(
+                                        dataEntries: intListToRadarEntry(data[1]),
+                                        borderColor: settings.sysColor,
+                                        fillColor: settings.sysColor.withOpacity(opacity),
+                                        entryRadius: 0,
+                                        borderWidth: settings.graphLineThickness),
+                                    RadarDataSet(
+                                        dataEntries: intListToRadarEntry(data[2]),
+                                        borderColor: settings.pulColor,
+                                        fillColor: settings.pulColor.withOpacity(opacity),
+                                        entryRadius: 0,
+                                        borderWidth: settings.graphLineThickness),
+                                  ],
+                                ),
                               ),
-                            ),
-                          );
-                        })(),
-                      ),
-                    ],
-                  );
-                });
-              }
-          );
-        },
-      )),
+                            );
+                          })(),
+                        ),
+                      ],
+                    );
+                  }
+              );
+            });
+          },
+        )),
+      ),
+      floatingActionButton: const SizedBox(
+        height: 70,
+        child: Center(child: IntervalPicker()),
+      ),
     );
   }
 
test/model/analyzer_test.dart
@@ -61,5 +61,7 @@ void main() {
       expect((m.firstDay), DateTime.fromMillisecondsSinceEpoch(-2200));
       expect((m.lastDay), DateTime.fromMillisecondsSinceEpoch(9000000));
     });
+
+    // TODO null tests, test with 1 element
   });
 }
\ No newline at end of file
test/ui/statistics_test.dart
@@ -10,12 +10,12 @@ import 'package:provider/provider.dart';
 void main() {
   group("StatisticsPage", () {
     testWidgets('should load page', (widgetTester) async {
-      await _initStatsPage(widgetTester, []);
+      await _initStatsPage(widgetTester, RamSettings(), []);
       expect(find.text('Statistics'), findsOneWidget);
     });
     testWidgets("should report measurement count", (widgetTester) async {
-      await _initStatsPage(widgetTester, [
-        for (int i = 0; i<50; i++)
+      await _initStatsPage(widgetTester, _allMeasurements(), [
+        for (int i = 1; i<51; i++) // can't safe entries at or before epoch
           BloodPressureRecord(DateTime.fromMillisecondsSinceEpoch(i), 40+i, 60+i, 30+i, 'Test comment $i'),
       ]);
       final measurementCountWidget = find.byKey(const Key('measurementCount'));
@@ -25,9 +25,8 @@ void main() {
   });
 }
 
-Future<void> _initStatsPage(WidgetTester widgetTester, List<BloodPressureRecord> records) async {
+Future<void> _initStatsPage(WidgetTester widgetTester, Settings settings, List<BloodPressureRecord> records) async {
   final model = RamBloodPressureModel();
-  final settings = RamSettings();
 
   for (var r in records) {
     model.add(r);
@@ -45,4 +44,10 @@ Future<void> _initStatsPage(WidgetTester widgetTester, List<BloodPressureRecord>
       )
   ));
   await widgetTester.pumpAndSettle();
+}
+
+RamSettings _allMeasurements() {
+  final settings = RamSettings();
+  settings.changeStepSize(TimeStep.lifetime);
+  return settings;
 }
\ No newline at end of file