Skip to content

Commit

Permalink
Merge pull request #148 from NobodyForNothing/FEAT-needle-pins
Browse files Browse the repository at this point in the history
Implement needle pins for entries
  • Loading branch information
derdilla authored Sep 17, 2023
2 parents 1080b01 + 0fa2b7c commit 8ad519d
Show file tree
Hide file tree
Showing 6 changed files with 372 additions and 97 deletions.
53 changes: 39 additions & 14 deletions lib/components/measurement_graph.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,24 +80,13 @@ class _LineChartState extends State<_LineChart> {
LineChartData(
minY: minValue.toDouble(),
maxY: maxValue + 5,
clipData: FlClipData.all(),
titlesData: _buildFlTitlesData(settings),
lineTouchData: const LineTouchData(
touchTooltipData: LineTouchTooltipData(tooltipMargin: -200, tooltipRoundedRadius: 20)
),
lineBarsData: [
_buildBarData(animatedThickness, sysSpots, settings.sysColor, true, settings.sysWarn.toDouble()),
_buildBarData(animatedThickness, diaSpots, settings.diaColor, true, settings.diaWarn.toDouble()),
_buildBarData(animatedThickness, pulSpots, settings.pulColor, false),
if (settings.drawRegressionLines)
_buildRegressionLine(sysSpots),
if (settings.drawRegressionLines)
_buildRegressionLine(diaSpots),
if (settings.drawRegressionLines)
_buildRegressionLine(pulSpots),
for (final horizontalLine in settings.horizontalGraphLines)
if (horizontalLine.height < maxValue && horizontalLine.height > minValue)
_buildHorizontalLine(horizontalLine, graphBegin!, graphEnd!),
]
lineBarsData: buildBars(animatedThickness, settings, sysSpots, diaSpots, pulSpots,
maxValue, minValue, graphBegin, graphEnd, fetchedData)
),
);
},
Expand All @@ -110,6 +99,26 @@ class _LineChartState extends State<_LineChart> {
);
}

List<LineChartBarData> buildBars(double animatedThickness, Settings settings, List<FlSpot> sysSpots, List<FlSpot> diaSpots, List<FlSpot> pulSpots, int maxValue, int minValue, double? graphBegin, double? graphEnd, Iterable<BloodPressureRecord> allRecords) {
var bars = [
_buildBarData(animatedThickness, sysSpots, settings.sysColor, true, settings.sysWarn.toDouble()),
_buildBarData(animatedThickness, diaSpots, settings.diaColor, true, settings.diaWarn.toDouble()),
_buildBarData(animatedThickness, pulSpots, settings.pulColor, false),
for (final horizontalLine in settings.horizontalGraphLines)
if (horizontalLine.height < maxValue && horizontalLine.height > minValue)
_buildHorizontalLine(horizontalLine, graphBegin!, graphEnd!),
];
if (settings.drawRegressionLines) {
bars.addAll([
_buildRegressionLine(sysSpots),
_buildRegressionLine(diaSpots),
_buildRegressionLine(pulSpots),
]);
}
bars.addAll(_buildNeedlePins(allRecords, minValue, maxValue));
return bars;
}

FlTitlesData _buildFlTitlesData(Settings settings) {
const noTitels = AxisTitles(sideTitles: SideTitles(reservedSize: 40, showTitles: false));
return FlTitlesData(
Expand Down Expand Up @@ -169,6 +178,22 @@ class _LineChartState extends State<_LineChart> {
);
}

List<LineChartBarData> _buildNeedlePins(Iterable<BloodPressureRecord> allRecords, int min, int max) {
final pins = <LineChartBarData>[];
for (final r in allRecords.where((e) => e.needlePin != null)) {
pins.add(LineChartBarData(
spots: [
FlSpot(r.creationTime.millisecondsSinceEpoch.toDouble(), min.toDouble()),
FlSpot(r.creationTime.millisecondsSinceEpoch.toDouble(), max + 5)
],
barWidth: 20,
dotData: FlDotData(show: false),
color: r.needlePin!.color.withAlpha(100),
));
}
return pins;
}

LineChartBarData _buildBarData(double lineThickness, List<FlSpot> spots, Color color, bool hasAreaData, [double? areaDataCutOff]) {
return LineChartBarData(
spots: spots,
Expand Down
9 changes: 6 additions & 3 deletions lib/components/measurement_list/measurement_list_entry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class MeasurementListRow extends StatelessWidget {
// Leading color possible
title: buildRow(formatter),
childrenPadding: const EdgeInsets.only(bottom: 10),
backgroundColor: record.needlePin?.color.withAlpha(30),
collapsedShape: record.needlePin != null ? Border(left: BorderSide(color: record.needlePin!.color, width: 8)) : null,
children: [
ListTile(
subtitle: Text(formatter.format(record.creationTime)),
Expand Down Expand Up @@ -53,19 +55,20 @@ class MeasurementListRow extends StatelessWidget {
}

Row buildRow(DateFormat formatter) {
String formatNum(int? num) => (num ?? '-').toString();
return Row(
children: [
Expanded(
flex: 3,
child: Text(record.systolic.toString()),
child: Text(formatNum(record.systolic)),
),
Expanded(
flex: 3,
child: Text(record.diastolic.toString()),
child: Text(formatNum(record.diastolic)),
),
Expanded(
flex: 3,
child: Text(record.pulse.toString()),
child: Text(formatNum(record.pulse)),
),
]
);
Expand Down
4 changes: 3 additions & 1 deletion lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -425,5 +425,7 @@
"timestamp": "Timestamp",
"@timestamp": {},
"note": "Note",
"@note": {}
"@note": {},
"color": "Color",
"@color": {}
}
98 changes: 73 additions & 25 deletions lib/model/blood_pressure.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:blood_pressure_app/screens/error_reporting.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';

Expand All @@ -17,17 +20,47 @@ class BloodPressureModel extends ChangeNotifier {
dbPath = join(dbPath, 'blood_pressure.db');
}

// In case safer data loading is needed: finish this.
/*
String? backupPath;
if (dbPath != inMemoryDatabasePath) {
assert(_database.isUndefinedOrNull);
backupPath = join(Directory.systemTemp.path, 'blood_pressure_bu_${DateTime.now().millisecondsSinceEpoch}.db');
final copiedFile = File(dbPath).copy(backupPath);
copiedFile.onError((error, stackTrace) => null)
}
var preserveBackup = false;
*/

_database = await openDatabase(
dbPath,
// runs when the database is first created
onCreate: (db, version) {
return db.execute(
'CREATE TABLE bloodPressureModel(timestamp INTEGER(14) PRIMARY KEY, systolic INTEGER, diastolic INTEGER, pulse INTEGER, notes STRING)');
},
version: 1,
onCreate: _onDBCreate,
onUpgrade: _onDBUpgrade,
version: 2,
);
}

FutureOr<void> _onDBCreate(Database db, int version) {
return db.execute('CREATE TABLE bloodPressureModel('
'timestamp INTEGER(14) PRIMARY KEY,'
'systolic INTEGER, diastolic INTEGER,'
'pulse INTEGER,'
'notes STRING,'
'needlePin STRING)');
}

FutureOr<void> _onDBUpgrade(Database db, int oldVersion, int newVersion) async {
// When adding more versions the upgrade procedure proposed in https://stackoverflow.com/a/75153875/21489239
// might be useful, to avoid duplicated code. Currently this would only lead to complexity, without benefits.
if (oldVersion == 1 && newVersion == 2) {
db.execute('ALTER TABLE bloodPressureModel ADD COLUMN needlePin STRING;');
db.database.setVersion(2);
} else {
await ErrorReporting.reportCriticalError('Unsupported database upgrade', 'Attempted to upgrade the measurement database from version $oldVersion to version $newVersion, which is not supported. This action failed to avoid data loss. Please contact the app developer by opening an issue with the link below or writing an email to [email protected].');
}
}

// factory method, to allow for async constructor
static Future<BloodPressureModel> create({String? dbPath, bool isFullPath = false}) async {
if (Platform.isWindows || Platform.isLinux) {
Expand All @@ -51,7 +84,8 @@ class BloodPressureModel extends ChangeNotifier {
'systolic': measurement.systolic,
'diastolic': measurement.diastolic,
'pulse': measurement.pulse,
'notes': measurement.notes
'notes': measurement.notes,
'needlePin': jsonEncode(measurement.needlePin)
},
where: 'timestamp = ?',
whereArgs: [measurement.creationTime.millisecondsSinceEpoch]);
Expand All @@ -61,7 +95,8 @@ class BloodPressureModel extends ChangeNotifier {
'systolic': measurement.systolic,
'diastolic': measurement.diastolic,
'pulse': measurement.pulse,
'notes': measurement.notes
'notes': measurement.notes,
'needlePin': jsonEncode(measurement.needlePin)
});
}
notifyListeners();
Expand Down Expand Up @@ -93,37 +128,37 @@ class BloodPressureModel extends ChangeNotifier {
List<BloodPressureRecord> _convert(List<Map<String, Object?>> dbResult) {
List<BloodPressureRecord> records = [];
for (var e in dbResult) {
records.add(BloodPressureRecord(DateTime.fromMillisecondsSinceEpoch(e['timestamp'] as int), e['systolic'] as int?,
e['diastolic'] as int?, e['pulse'] as int?, e['notes'].toString()));
final needlePinJson = e['needlePin'] as String?;
records.add(BloodPressureRecord(
DateTime.fromMillisecondsSinceEpoch(e['timestamp'] as int),
e['systolic'] as int?,
e['diastolic'] as int?,
e['pulse'] as int?,
e['notes'].toString(),
needlePin: (needlePinJson == null) ? null : MeasurementNeedlePin.fromJson(jsonDecode(needlePinJson))
));
}
return records;
}
}

@immutable
class BloodPressureRecord {
late final DateTime _creationTime;
late final DateTime creationTime;
final int? systolic;
final int? diastolic;
final int? pulse;
final String notes;
//TODO: when adding a color / needle pin for entries:
// - the whole row in the table can be with that bg color
// - add lots of test to make sure this doesn't break records
// - maybe even store independently

BloodPressureRecord(DateTime creationTime, this.systolic, this.diastolic, this.pulse, this.notes) {
this.creationTime = creationTime;
}
final MeasurementNeedlePin? needlePin;

DateTime get creationTime => _creationTime;
/// datetime needs to be after epoch
set creationTime(DateTime value) {
if (value.millisecondsSinceEpoch > 0) {
_creationTime = value;
BloodPressureRecord(DateTime creationTime, this.systolic, this.diastolic, this.pulse, this.notes, {
this.needlePin
}) {
if (creationTime.millisecondsSinceEpoch > 0) {
this.creationTime = creationTime;
} else {
assert(false, "Tried to create BloodPressureRecord at or before epoch");
_creationTime = DateTime.fromMillisecondsSinceEpoch(1);
this.creationTime = DateTime.fromMillisecondsSinceEpoch(1);
}
}

Expand All @@ -133,6 +168,19 @@ class BloodPressureRecord {
}
}

@immutable
class MeasurementNeedlePin {
final Color color;

const MeasurementNeedlePin(this.color);
// When updating this, remember to be backwards compatible
MeasurementNeedlePin.fromJson(Map<String, dynamic> json)
: color = Color(json['color']);
Map<String, dynamic> toJson() => {
'color': color.value,
};
}

// source: https://pressbooks.library.torontomu.ca/vitalsign/chapter/blood-pressure-ranges/ (last access: 20.05.2023)
class BloodPressureWarnValues {
BloodPressureWarnValues._create();
Expand Down
Loading

0 comments on commit 8ad519d

Please sign in to comment.