Commit 7f35b00
Changed files (5)
lib
screens
subsettings
test
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