393 lines
14 KiB
Dart
393 lines
14 KiB
Dart
import 'package:charts_flutter/flutter.dart' as charts;
|
|
import 'package:diameter/localization_keys.dart';
|
|
import 'package:diameter/models/basal.dart';
|
|
import 'package:diameter/models/glucose_target.dart';
|
|
import 'package:diameter/models/log_bolus.dart';
|
|
import 'package:diameter/models/log_entry.dart';
|
|
import 'package:diameter/models/log_event.dart';
|
|
import 'package:diameter/models/log_meal.dart';
|
|
import 'package:diameter/models/settings.dart';
|
|
import 'package:diameter/navigation.dart';
|
|
import 'package:diameter/utils/date_time_utils.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_translate/flutter_translate.dart';
|
|
|
|
charts.TimeSeriesChart generateChart(
|
|
DateTime date,
|
|
List<LogEntry> logEntries,
|
|
List<LogEvent> logEvents,
|
|
List<GlucoseTarget> targets,
|
|
bool showMeals,
|
|
bool showBasal,
|
|
bool showBolus) {
|
|
final targetRange = targets.singleWhere((target) =>
|
|
Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl
|
|
? Settings.targetMgPerDl >= target.fromMgPerDL &&
|
|
Settings.targetMgPerDl <= target.toMgPerDl
|
|
: Settings.targetMmolPerL >= target.fromMmolPerL &&
|
|
Settings.targetMmolPerL <= target.toMmolPerL);
|
|
|
|
final List<Basal> basalRate = [];
|
|
|
|
if (showBasal) {
|
|
Basal? lastBasal;
|
|
|
|
for (var time = date;
|
|
time.isBefore(date.add(const Duration(days: 1)));
|
|
time = time.add(const Duration(minutes: 1))) {
|
|
final basal = Basal.getRateForTime(time);
|
|
if (basal != null && basal.units != lastBasal?.units) {
|
|
if (basalRate.isNotEmpty) {
|
|
basalRate.last.endTime = time;
|
|
}
|
|
basalRate.add(
|
|
Basal(startTime: time, endTime: basal.endTime, units: basal.units));
|
|
lastBasal = basal;
|
|
}
|
|
}
|
|
}
|
|
|
|
return charts.TimeSeriesChart(
|
|
[
|
|
charts.Series<LogEntry, DateTime>(
|
|
id: translate(LocalizationKeys.reports_ids_glucose),
|
|
colorFn: (LogEntry entry, _) => charts.Color.black,
|
|
domainFn: (LogEntry entry, _) => entry.time,
|
|
measureFn: (LogEntry entry, _) =>
|
|
Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl
|
|
? entry.mgPerDl
|
|
: entry.mmolPerL,
|
|
data: logEntries,
|
|
)..setAttribute(charts.rendererIdKey, 'glucoseRenderer'),
|
|
charts.Series<LogEntry, DateTime>(
|
|
id: translate(LocalizationKeys.reports_ids_carbs),
|
|
colorFn: (LogEntry entry, _) =>
|
|
charts.MaterialPalette.yellow.shadeDefault,
|
|
domainFn: (LogEntry entry, _) => entry.time,
|
|
measureFn: (LogEntry entry, _) =>
|
|
LogMeal.getTotalCarbsForEntry(entry.id),
|
|
data: showMeals ? logEntries : [],
|
|
)..setAttribute(charts.rendererIdKey, 'carbsRenderer'),
|
|
charts.Series<Basal, DateTime>(
|
|
id: translate(LocalizationKeys.reports_ids_basal),
|
|
colorFn: (Basal basal, _) => charts.Color.black.lighter,
|
|
domainFn: (Basal basal, _) => basal.startTime,
|
|
measureFn: (Basal basal, _) => basal.units,
|
|
data: showBasal ? basalRate : [],
|
|
)
|
|
..setAttribute(
|
|
charts.measureAxisIdKey, charts.Axis.secondaryMeasureAxisId)
|
|
..setAttribute(charts.rendererIdKey, 'basalRenderer'),
|
|
charts.Series<LogEntry, DateTime>(
|
|
id: translate(LocalizationKeys.reports_ids_bolus),
|
|
colorFn: (LogEntry entry, _) =>
|
|
charts.MaterialPalette.blue.shadeDefault,
|
|
domainFn: (LogEntry entry, _) => entry.time,
|
|
measureFn: (LogEntry entry, _) =>
|
|
LogBolus.getTotalBolusForEntry(entry.id),
|
|
data: showBolus ? logEntries : [],
|
|
)
|
|
..setAttribute(
|
|
charts.measureAxisIdKey, charts.Axis.secondaryMeasureAxisId)
|
|
..setAttribute(charts.rendererIdKey, 'bolusRenderer'),
|
|
],
|
|
domainAxis: const charts.DateTimeAxisSpec(
|
|
tickFormatterSpec: charts.AutoDateTimeTickFormatterSpec(
|
|
hour: charts.TimeFormatterSpec(
|
|
format: 'HH',
|
|
transitionFormat: 'HH',
|
|
),
|
|
),
|
|
),
|
|
primaryMeasureAxis: const charts.NumericAxisSpec(
|
|
tickProviderSpec: charts.BasicNumericTickProviderSpec(
|
|
desiredTickCount: 5,
|
|
),
|
|
),
|
|
secondaryMeasureAxis: const charts.NumericAxisSpec(
|
|
tickProviderSpec: charts.BasicNumericTickProviderSpec(
|
|
desiredTickCount: 5,
|
|
),
|
|
),
|
|
animate: false,
|
|
customSeriesRenderers: [
|
|
charts.LineRendererConfig(
|
|
includeLine: true,
|
|
includePoints: false,
|
|
customRendererId: 'glucoseRenderer',
|
|
),
|
|
charts.LineRendererConfig(
|
|
includeLine: true,
|
|
includePoints: false,
|
|
includeArea: true,
|
|
roundEndCaps: true,
|
|
strokeWidthPx: 1,
|
|
customRendererId: 'basalRenderer',
|
|
),
|
|
charts.LineRendererConfig(
|
|
includeLine: false,
|
|
includePoints: true,
|
|
customRendererId: 'bolusRenderer',
|
|
),
|
|
charts.LineRendererConfig(
|
|
includeLine: false,
|
|
includePoints: true,
|
|
customRendererId: 'carbsRenderer',
|
|
),
|
|
],
|
|
behaviors: [
|
|
charts.RangeAnnotation([
|
|
charts.RangeAnnotationSegment<num>(
|
|
Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl
|
|
? targetRange.fromMgPerDL
|
|
: targetRange.fromMmolPerL,
|
|
Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl
|
|
? targetRange.toMgPerDl
|
|
: targetRange.toMmolPerL,
|
|
charts.RangeAnnotationAxisType.measure,
|
|
color: charts.MaterialPalette.green.shadeDefault.lighter,
|
|
),
|
|
...logEvents
|
|
.where((element) => element.hasEndTime)
|
|
.map((LogEvent event) => charts.RangeAnnotationSegment<DateTime>(
|
|
event.time,
|
|
event.endTime ?? DateTime.now(),
|
|
charts.RangeAnnotationAxisType.domain,
|
|
startLabel: event.eventType.target?.value,
|
|
color: charts.MaterialPalette.gray.shade300,
|
|
))
|
|
.toList(),
|
|
...logEvents
|
|
.where((element) => !element.hasEndTime)
|
|
.map((LogEvent event) => charts.LineAnnotationSegment<DateTime>(
|
|
event.time,
|
|
charts.RangeAnnotationAxisType.domain,
|
|
startLabel: '${event.eventType.target?.value}',
|
|
))
|
|
.toList(),
|
|
]),
|
|
],
|
|
);
|
|
}
|
|
|
|
class DailyChart extends StatefulWidget {
|
|
static const String routeName = '/reports/base';
|
|
const DailyChart({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
_DailyChartState createState() => _DailyChartState();
|
|
}
|
|
|
|
class _DailyChartState extends State<DailyChart> {
|
|
late List<LogEntry> logEntries;
|
|
late List<LogEvent> logEvents;
|
|
late List<GlucoseTarget> targets;
|
|
|
|
final ScrollController _scrollController = ScrollController();
|
|
final TextEditingController _dateController = TextEditingController(text: '');
|
|
|
|
late DateTime date;
|
|
|
|
String? swipeDirection;
|
|
|
|
bool showChart = true;
|
|
bool showBolus = true;
|
|
bool showBasal = true;
|
|
bool showMeals = true;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
date = DateTimeUtils.today();
|
|
logEntries = LogEntry.getAllForDate(date);
|
|
logEvents = LogEvent.getAllForDate(date);
|
|
targets = GlucoseTarget.getAll();
|
|
_dateController.text = DateTimeUtils.displayDate(date);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_scrollController.dispose();
|
|
_dateController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void reload({String? message}) {
|
|
setState(() {
|
|
logEntries = LogEntry.getAllForDate(date);
|
|
logEvents = LogEvent.getAllForDate(date);
|
|
});
|
|
setState(() {
|
|
if (message != null) {
|
|
var snackBar = SnackBar(
|
|
content: Text(message),
|
|
duration: const Duration(seconds: 2),
|
|
);
|
|
ScaffoldMessenger.of(context)
|
|
..removeCurrentSnackBar()
|
|
..showSnackBar(snackBar);
|
|
}
|
|
});
|
|
}
|
|
|
|
void onChangeDate(DateTime? newDate) {
|
|
if (newDate != null) {
|
|
setState(() {
|
|
date = DateTime(newDate.year, newDate.month, newDate.day);
|
|
_dateController.text = DateTimeUtils.displayDate(newDate);
|
|
});
|
|
reload();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Daily Chart'),
|
|
actions: <Widget>[
|
|
IconButton(
|
|
onPressed: () => onChangeDate(DateTime.now()),
|
|
icon: const Icon(Icons.today)),
|
|
IconButton(
|
|
onPressed: () => showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
CheckboxListTile(
|
|
value: showChart,
|
|
onChanged: (_) =>
|
|
setState(() => showChart = !showChart),
|
|
title: Text(translate(LocalizationKeys.reports_dailyCharts_showChart)),
|
|
controlAffinity: ListTileControlAffinity.leading,
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 20.0),
|
|
child: CheckboxListTile(
|
|
value: showBolus,
|
|
onChanged: showChart
|
|
? (_) =>
|
|
setState(() => showBolus = !showBolus)
|
|
: null,
|
|
title: Text(translate(LocalizationKeys.reports_dailyCharts_showBolus)),
|
|
controlAffinity:
|
|
ListTileControlAffinity.leading,
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 20.0),
|
|
child: CheckboxListTile(
|
|
value: showBasal,
|
|
onChanged: showChart
|
|
? (_) =>
|
|
setState(() => showBasal = !showBasal)
|
|
: null,
|
|
title: Text(translate(LocalizationKeys.reports_dailyCharts_showBasal)),
|
|
controlAffinity:
|
|
ListTileControlAffinity.leading,
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 20.0),
|
|
child: CheckboxListTile(
|
|
value: showMeals,
|
|
onChanged: showChart
|
|
? (_) =>
|
|
setState(() => showMeals = !showMeals)
|
|
: null,
|
|
title: Text(translate(LocalizationKeys.reports_dailyCharts_showMeals)),
|
|
controlAffinity:
|
|
ListTileControlAffinity.leading,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
actions: <Widget>[
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: Text(translate(LocalizationKeys.general_close).toUpperCase()),
|
|
),
|
|
]),
|
|
),
|
|
icon: const Icon(Icons.settings)),
|
|
IconButton(
|
|
onPressed: reload,
|
|
icon: const Icon(Icons.refresh),
|
|
),
|
|
],
|
|
),
|
|
drawer: const Navigation(currentLocation: DailyChart.routeName),
|
|
body: GestureDetector(
|
|
onPanUpdate: (details) {
|
|
swipeDirection = details.delta.dx < 0 ? 'left' : 'right';
|
|
},
|
|
onPanEnd: (details) {
|
|
if (swipeDirection == null) {
|
|
return;
|
|
}
|
|
if (swipeDirection == 'right' &&
|
|
!date.isAtSameMomentAs(DateTime(2000, 1, 1))) {
|
|
onChangeDate(date.subtract(const Duration(days: 1)));
|
|
}
|
|
if (swipeDirection == 'left' &&
|
|
date.add(const Duration(days: 1)).isBefore(DateTime.now())) {
|
|
onChangeDate(date.add(const Duration(days: 1)));
|
|
}
|
|
},
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
IconButton(
|
|
onPressed: date.isAtSameMomentAs(DateTime(2000, 1, 1))
|
|
? null
|
|
: () =>
|
|
onChangeDate(date.subtract(const Duration(days: 1))),
|
|
icon: const Icon(Icons.arrow_back),
|
|
),
|
|
Expanded(
|
|
child: GestureDetector(
|
|
onTap: () async {
|
|
final newTime = await showDatePicker(
|
|
context: context,
|
|
initialDate: date,
|
|
firstDate: DateTime(2000, 1, 1),
|
|
lastDate: DateTime.now().add(const Duration(days: 365)),
|
|
);
|
|
onChangeDate(newTime);
|
|
},
|
|
child: Text(
|
|
DateTimeUtils.displayDate(date).toUpperCase(),
|
|
style: Theme.of(context).textTheme.subtitle2,
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed:
|
|
date.add(const Duration(days: 1)).isBefore(DateTime.now())
|
|
? () => onChangeDate(date.add(const Duration(days: 1)))
|
|
: null,
|
|
icon: const Icon(Icons.arrow_forward),
|
|
),
|
|
],
|
|
),
|
|
Expanded(
|
|
child: logEntries.isNotEmpty
|
|
? generateChart(date, logEntries, logEvents, targets, showMeals,
|
|
showBasal, showBolus)
|
|
: Center(
|
|
child: Text(translate(LocalizationKeys.reports_dailyCharts_empty)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|