479 lines
16 KiB
Dart
479 lines
16 KiB
Dart
![]() |
import 'package:fl_chart/fl_chart.dart';
|
||
|
import 'package:flutter/material.dart';
|
||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
|
import 'package:habitrack_app/infrastructure/widget_wall/items_state.dart';
|
||
|
import 'package:habitrack_app/main.dart';
|
||
|
import 'package:habitrack_app/sembast/hydration.dart';
|
||
|
import 'package:intl/intl.dart';
|
||
|
|
||
|
class DataPoint {
|
||
|
DataPoint({required this.date, required this.progress});
|
||
|
DateTime date;
|
||
|
double progress;
|
||
|
}
|
||
|
|
||
|
class HydrationGraphWidget extends ConsumerStatefulWidget {
|
||
|
const HydrationGraphWidget({super.key});
|
||
|
|
||
|
@override
|
||
|
ConsumerState<ConsumerStatefulWidget> createState() =>
|
||
|
_TasksGraphWidgetState();
|
||
|
}
|
||
|
|
||
|
class _TasksGraphWidgetState extends ConsumerState<HydrationGraphWidget> {
|
||
|
void _buildList(List<dynamic> value) {
|
||
|
final items = value;
|
||
|
_thisWeekCompleted = 0;
|
||
|
_thisWeekPlanned = 0;
|
||
|
_maxAmount = 0;
|
||
|
|
||
|
setState(() {
|
||
|
_todayCompleted = 0;
|
||
|
_todayPlanned = 0;
|
||
|
_weeklyPlannedEntries = [];
|
||
|
_weeklyWorkedEntries = [];
|
||
|
for (var i = 0; i <= 6; i++) {
|
||
|
final itemToInsert = DataPoint(
|
||
|
date: _latestDate.subtract(Duration(days: 6 - i)),
|
||
|
progress: 0,
|
||
|
);
|
||
|
|
||
|
_weeklyPlannedEntries.add(itemToInsert);
|
||
|
}
|
||
|
|
||
|
for (var i = 0; i <= 6; i++) {
|
||
|
final itemToInsert = DataPoint(
|
||
|
date: _latestDate.subtract(Duration(days: 6 - i)),
|
||
|
progress: 0,
|
||
|
);
|
||
|
_weeklyWorkedEntries.add(itemToInsert);
|
||
|
}
|
||
|
});
|
||
|
_thisWeekPlanned = 0;
|
||
|
|
||
|
for (final item in items) {
|
||
|
if (_selectedValue == 'weekly') {
|
||
|
var alreadyAdded = false;
|
||
|
for (var i = 0; i <= 6; i++) {
|
||
|
DateTime parsedDate;
|
||
|
|
||
|
if (item is Hydration) {
|
||
|
if (item.current / item.goal >= 1 && item.completedOn != '') {
|
||
|
//item is completed
|
||
|
parsedDate = DateTime.parse(item.completedOn);
|
||
|
} else {
|
||
|
parsedDate = DateTime.parse(item.createdOn);
|
||
|
logger.i('GOAL: $item.goal');
|
||
|
}
|
||
|
|
||
|
logger
|
||
|
.i('BEFORE $i days ago and AFTER ${i + 1} days ago, element at '
|
||
|
'index ${6 - i} will be updated.');
|
||
|
|
||
|
if (parsedDate.isBefore(_latestDate.subtract(Duration(days: i))) &&
|
||
|
parsedDate
|
||
|
.isAfter(_latestDate.subtract(Duration(days: i + 1)))) {
|
||
|
logger.i('LOOPING');
|
||
|
if (!alreadyAdded) {
|
||
|
alreadyAdded = true;
|
||
|
_thisWeekCompleted += item.current / 1000;
|
||
|
|
||
|
_thisWeekPlanned += item.goal / 1000;
|
||
|
}
|
||
|
|
||
|
if (item.goal > _maxAmount) {
|
||
|
_maxAmount = (item.goal.toDouble() / 1000).ceilToDouble();
|
||
|
}
|
||
|
if (item.current > _maxAmount && item.current > item.goal) {
|
||
|
_maxAmount = (item.current.toDouble() / 1000).ceilToDouble();
|
||
|
}
|
||
|
// Update maxAmount
|
||
|
setState(() {
|
||
|
_weeklyPlannedEntries.elementAt(6 - i).progress =
|
||
|
item.goal.toDouble() / 1000;
|
||
|
_weeklyWorkedEntries.elementAt(6 - i).progress =
|
||
|
item.current.toDouble() / 1000;
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
} else if (_selectedValue == 'daily' && item is Hydration) {
|
||
|
logger.i('DATES');
|
||
|
final parsedDate = DateTime.parse(item.createdOn);
|
||
|
logger.i('LATEST DATE: $_latestDate');
|
||
|
|
||
|
final oneDayAgo = _latestDate.subtract(const Duration(days: 1));
|
||
|
|
||
|
if (parsedDate.isBefore(oneDayAgo)) {
|
||
|
logger.i('More than a day old');
|
||
|
} else if (parsedDate.isAfter(oneDayAgo) &&
|
||
|
parsedDate.isBefore(_latestDate)) {
|
||
|
logger.i('TOday');
|
||
|
_todayCompleted = item.current / 1000;
|
||
|
_todayPlanned = item.goal / 1000;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void _showPreviousWeek() {
|
||
|
setState(() {
|
||
|
_todayPlanned = 0;
|
||
|
_todayCompleted = 0;
|
||
|
_thisWeekPlanned = 0;
|
||
|
_thisWeekCompleted = 0;
|
||
|
});
|
||
|
|
||
|
if (_selectedValue == 'weekly') {
|
||
|
_latestDate = _latestDate.subtract(const Duration(days: 7));
|
||
|
} else if (_selectedValue == 'daily') {
|
||
|
logger.i('HMMM');
|
||
|
_latestDate = _latestDate.subtract(const Duration(days: 1));
|
||
|
}
|
||
|
final items = ref.watch(itemsProvider);
|
||
|
|
||
|
switch (items) {
|
||
|
case AsyncError(:final error):
|
||
|
logger.i('Error: $error');
|
||
|
case AsyncData(:final value):
|
||
|
final allItems = value;
|
||
|
|
||
|
_buildList(allItems);
|
||
|
default:
|
||
|
logger.i('Hmmm, how can we help?');
|
||
|
} // get current date
|
||
|
// show past 7 days starting 14 days ago
|
||
|
}
|
||
|
|
||
|
void _showNextWeek() {
|
||
|
if (_selectedValue == 'weekly') {
|
||
|
_latestDate = _latestDate.add(const Duration(days: 7));
|
||
|
} else if (_selectedValue == 'daily') {
|
||
|
logger.i('HMMM');
|
||
|
_latestDate = _latestDate.add(const Duration(days: 1));
|
||
|
}
|
||
|
if (!_latestDate.isAfter(DateTime.now())) {
|
||
|
final items = ref.watch(itemsProvider);
|
||
|
|
||
|
switch (items) {
|
||
|
case AsyncError(:final error):
|
||
|
logger.i('Error: $error');
|
||
|
case AsyncData(:final value):
|
||
|
final allItems = value;
|
||
|
|
||
|
_buildList(allItems);
|
||
|
default:
|
||
|
logger.i('Hmmm, how can we help?');
|
||
|
} // get current date
|
||
|
}
|
||
|
|
||
|
// show past 7 days starting 14 days ago
|
||
|
}
|
||
|
|
||
|
String _getText() {
|
||
|
final dateFormat = DateFormat('dd. MMM yyyy');
|
||
|
|
||
|
if (_selectedValue == 'weekly' &&
|
||
|
_latestDate.isAfter(DateTime.now().subtract(const Duration(days: 6)))) {
|
||
|
return ' this past week';
|
||
|
} else if (_selectedValue == 'weekly' &&
|
||
|
!_latestDate
|
||
|
.isAfter(DateTime.now().subtract(const Duration(days: 6)))) {
|
||
|
return ' '
|
||
|
'from'
|
||
|
' ${dateFormat.format(_latestDate.subtract(const Duration(days: 6)))}'
|
||
|
' to ${dateFormat.format(_latestDate)}';
|
||
|
}
|
||
|
final formattedDate = dateFormat.format(_latestDate);
|
||
|
// return ' on ${_latestDate.toString().substring(6, 10)}';
|
||
|
return ' on $formattedDate';
|
||
|
}
|
||
|
|
||
|
List<dynamic> thisWeekItems = [];
|
||
|
List<dynamic> todayItems = [];
|
||
|
String? _selectedValue = 'weekly';
|
||
|
|
||
|
double _thisWeekCompleted = 0;
|
||
|
double _thisWeekPlanned = 0;
|
||
|
double _maxAmount = 0;
|
||
|
DateTime _latestDate = DateTime.now();
|
||
|
double _todayCompleted = 0;
|
||
|
double _todayPlanned = 0;
|
||
|
|
||
|
List<DataPoint> _weeklyPlannedEntries = <DataPoint>[];
|
||
|
List<DataPoint> _weeklyWorkedEntries = <DataPoint>[];
|
||
|
|
||
|
@override
|
||
|
Widget build(BuildContext context) {
|
||
|
final items = ref.watch(itemsProvider);
|
||
|
|
||
|
switch (items) {
|
||
|
case AsyncError(:final error):
|
||
|
logger.i('Error: $error');
|
||
|
case AsyncData(:final value):
|
||
|
final allItems = value;
|
||
|
_buildList(allItems);
|
||
|
default:
|
||
|
logger.i('Hmmm, how can we help?');
|
||
|
}
|
||
|
|
||
|
final firstDate = _weeklyPlannedEntries.elementAtOrNull(0)!.date;
|
||
|
logger.i('HMMM $firstDate');
|
||
|
final flSpots = <FlSpot>[];
|
||
|
for (final dataPoint in _weeklyPlannedEntries) {
|
||
|
final xValue = _daysBetween(_weeklyPlannedEntries[0].date, dataPoint.date)
|
||
|
.toDouble();
|
||
|
|
||
|
final lcbd = FlSpot(
|
||
|
xValue,
|
||
|
dataPoint.progress,
|
||
|
);
|
||
|
|
||
|
flSpots.add(lcbd);
|
||
|
}
|
||
|
|
||
|
final weeklyWorkedSpots = <FlSpot>[];
|
||
|
for (final dataPoint in _weeklyWorkedEntries) {
|
||
|
final xValue =
|
||
|
_daysBetween(_weeklyWorkedEntries[0].date, dataPoint.date).toDouble();
|
||
|
final lcbd = FlSpot(xValue, dataPoint.progress);
|
||
|
|
||
|
weeklyWorkedSpots.add(lcbd);
|
||
|
}
|
||
|
final gradientColors = [
|
||
|
Colors.cyan,
|
||
|
Colors.blueAccent,
|
||
|
];
|
||
|
final gradient2Colors = [
|
||
|
Colors.amber,
|
||
|
Colors.amberAccent,
|
||
|
];
|
||
|
final df = DateFormat('dd. MMMM');
|
||
|
|
||
|
return Column(
|
||
|
children: [
|
||
|
Container(
|
||
|
height: 75,
|
||
|
color: Theme.of(context).colorScheme.primaryContainer,
|
||
|
margin: const EdgeInsets.only(bottom: 10),
|
||
|
child: Row(
|
||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||
|
children: [
|
||
|
ElevatedButton(
|
||
|
onPressed: _showPreviousWeek,
|
||
|
style: ButtonStyle(
|
||
|
shape: WidgetStateProperty.all(
|
||
|
RoundedRectangleBorder(
|
||
|
borderRadius: BorderRadius.circular(10),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
child: const Text('Previous'),
|
||
|
),
|
||
|
DropdownButton<String>(
|
||
|
value: _selectedValue,
|
||
|
borderRadius: BorderRadius.circular(10),
|
||
|
onChanged: (value) {
|
||
|
setState(() {
|
||
|
_latestDate = DateTime.now();
|
||
|
_selectedValue = value;
|
||
|
});
|
||
|
},
|
||
|
items: const [
|
||
|
DropdownMenuItem(
|
||
|
value: 'daily',
|
||
|
child: Text('Daily'),
|
||
|
),
|
||
|
DropdownMenuItem(
|
||
|
value: 'weekly',
|
||
|
child: Text('Weekly'),
|
||
|
),
|
||
|
],
|
||
|
),
|
||
|
ElevatedButton(
|
||
|
onPressed: _showNextWeek,
|
||
|
style: ButtonStyle(
|
||
|
shape: WidgetStateProperty.all(
|
||
|
RoundedRectangleBorder(
|
||
|
borderRadius: BorderRadius.circular(10),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
child: const Text('Next'),
|
||
|
),
|
||
|
],
|
||
|
),
|
||
|
),
|
||
|
if (_selectedValue == 'weekly') ...[
|
||
|
Container(
|
||
|
padding:
|
||
|
const EdgeInsets.only(bottom: 15, top: 15, right: 15, left: 5),
|
||
|
child: SizedBox(
|
||
|
width: MediaQuery.of(context).size.width * 0.90,
|
||
|
height: MediaQuery.of(context).size.height * 0.5,
|
||
|
child: LineChart(
|
||
|
LineChartData(
|
||
|
minY: 0,
|
||
|
maxY: _maxAmount,
|
||
|
lineBarsData: [
|
||
|
LineChartBarData(
|
||
|
isCurved: true,
|
||
|
preventCurveOverShooting: true,
|
||
|
spots: flSpots,
|
||
|
belowBarData: BarAreaData(
|
||
|
show: true,
|
||
|
gradient: LinearGradient(
|
||
|
colors: gradientColors
|
||
|
.map((color) => color.withOpacity(0.6))
|
||
|
.toList(),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
LineChartBarData(
|
||
|
color: Colors.amber,
|
||
|
isCurved: true,
|
||
|
isStrokeCapRound: true,
|
||
|
preventCurveOverShooting: true,
|
||
|
spots: weeklyWorkedSpots,
|
||
|
belowBarData: BarAreaData(
|
||
|
show: true,
|
||
|
gradient: LinearGradient(
|
||
|
colors: gradient2Colors
|
||
|
.map((color) => color.withOpacity(0.6))
|
||
|
.toList(),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
],
|
||
|
titlesData: FlTitlesData(
|
||
|
rightTitles: const AxisTitles(
|
||
|
sideTitles: SideTitles(
|
||
|
reservedSize: 30,
|
||
|
interval: 20,
|
||
|
),
|
||
|
),
|
||
|
topTitles: const AxisTitles(
|
||
|
sideTitles: SideTitles(
|
||
|
interval: 20,
|
||
|
),
|
||
|
),
|
||
|
leftTitles: AxisTitles(
|
||
|
sideTitles: SideTitles(
|
||
|
showTitles: true,
|
||
|
reservedSize: 30,
|
||
|
getTitlesWidget: (value, meta) => Text(
|
||
|
'$value l',
|
||
|
style: const TextStyle(
|
||
|
fontSize: 10,
|
||
|
fontStyle: FontStyle.italic,
|
||
|
fontWeight: FontWeight.bold,
|
||
|
),
|
||
|
),
|
||
|
// interval: 0.25,
|
||
|
),
|
||
|
),
|
||
|
bottomTitles: AxisTitles(
|
||
|
sideTitles: SideTitles(
|
||
|
getTitlesWidget: (value, meta) => Text(
|
||
|
style: const TextStyle(
|
||
|
fontSize: 10,
|
||
|
fontStyle: FontStyle.italic,
|
||
|
fontWeight: FontWeight.bold,
|
||
|
),
|
||
|
df.format(
|
||
|
DateTime(
|
||
|
firstDate.year,
|
||
|
firstDate.month,
|
||
|
firstDate.day,
|
||
|
).add(Duration(days: value.round())),
|
||
|
),
|
||
|
),
|
||
|
showTitles: true,
|
||
|
interval: 1,
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
],
|
||
|
if (_selectedValue == 'daily') ...[
|
||
|
SizedBox(
|
||
|
width: MediaQuery.of(context).size.width * 0.90,
|
||
|
height: MediaQuery.of(context).size.height * 0.5,
|
||
|
child: PieChart(
|
||
|
PieChartData(
|
||
|
sectionsSpace: 0,
|
||
|
sections: [
|
||
|
PieChartSectionData(
|
||
|
value: _todayCompleted, // Progress
|
||
|
color: Theme.of(context).colorScheme.primaryFixedDim,
|
||
|
radius: 60,
|
||
|
title: (_todayPlanned > 0)
|
||
|
// ignore: lines_longer_than_80_chars
|
||
|
? '${((_todayCompleted / _todayPlanned) * 100).floorToDouble()} %'
|
||
|
: '0 %',
|
||
|
|
||
|
titleStyle: TextStyle(
|
||
|
fontSize: 15,
|
||
|
fontWeight: FontWeight.bold,
|
||
|
color: Theme.of(context).colorScheme.secondary,
|
||
|
),
|
||
|
),
|
||
|
PieChartSectionData(
|
||
|
titleStyle: TextStyle(
|
||
|
fontSize: 15,
|
||
|
fontWeight: FontWeight.bold,
|
||
|
color: Theme.of(context).colorScheme.primaryFixedDim,
|
||
|
),
|
||
|
value: (_todayPlanned > 0 && _todayCompleted > 0)
|
||
|
? (_todayPlanned - _todayCompleted)
|
||
|
: 1, // Total - progress
|
||
|
color: Theme.of(context).colorScheme.secondary,
|
||
|
radius: 60,
|
||
|
showTitle: _todayPlanned == 0 ||
|
||
|
(_todayPlanned > 0 && _todayCompleted == 0),
|
||
|
title: '0 %',
|
||
|
),
|
||
|
],
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
],
|
||
|
SizedBox(
|
||
|
width: MediaQuery.of(context).size.width * 0.90,
|
||
|
child: Column(
|
||
|
children: [
|
||
|
Text(
|
||
|
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||
|
fontSize: 15,
|
||
|
fontWeight: FontWeight.bold,
|
||
|
),
|
||
|
// ignore: lines_longer_than_80_chars
|
||
|
'${(_selectedValue == 'weekly') ? _thisWeekCompleted : _todayCompleted} liters drunk${_getText()}',
|
||
|
),
|
||
|
Text(
|
||
|
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||
|
fontSize: 15,
|
||
|
fontWeight: FontWeight.bold,
|
||
|
),
|
||
|
// ignore: lines_longer_than_80_chars
|
||
|
'Out of a total goal of ${(_selectedValue == 'weekly') ? _thisWeekPlanned : _todayPlanned} liters',
|
||
|
),
|
||
|
],
|
||
|
),
|
||
|
),
|
||
|
],
|
||
|
);
|
||
|
}
|
||
|
|
||
|
int _daysBetween(DateTime from, DateTime to) {
|
||
|
final d1 = DateTime(from.year, from.month, from.day);
|
||
|
final d2 = DateTime(to.year, to.month, to.day);
|
||
|
return (d2.difference(d1).inHours / 24).round();
|
||
|
}
|
||
|
}
|