diameter/lib/screens/reports/daily_chart.dart
2022-03-21 01:08:05 +01:00

392 lines
14 KiB
Dart

import 'package:charts_flutter/flutter.dart' as charts;
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';
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: '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: 'Carbohydrates',
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: '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: '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: const Text('show Chart'),
controlAffinity: ListTileControlAffinity.leading,
),
Padding(
padding: const EdgeInsets.only(left: 20.0),
child: CheckboxListTile(
value: showBolus,
onChanged: showChart
? (_) =>
setState(() => showBolus = !showBolus)
: null,
title: const Text('show Bolus'),
controlAffinity:
ListTileControlAffinity.leading,
),
),
Padding(
padding: const EdgeInsets.only(left: 20.0),
child: CheckboxListTile(
value: showBasal,
onChanged: showChart
? (_) =>
setState(() => showBasal = !showBasal)
: null,
title: const Text('show Basal'),
controlAffinity:
ListTileControlAffinity.leading,
),
),
Padding(
padding: const EdgeInsets.only(left: 20.0),
child: CheckboxListTile(
value: showMeals,
onChanged: showChart
? (_) =>
setState(() => showMeals = !showMeals)
: null,
title: const Text('show Meals'),
controlAffinity:
ListTileControlAffinity.leading,
),
),
],
),
actions: <Widget>[
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('CLOSE'),
),
]),
),
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)
: const Center(
child: Text(
'You have not created any Log Entries for this date yet!'),
),
),
],
),
),
);
}
}