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 logEntries, List logEvents, List 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 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( 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( 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( 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( 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( 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( 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( 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 { late List logEntries; late List logEvents; late List 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: [ 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: [ 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!'), ), ), ], ), ), ); } }