18
18
19
19
import 'package:fl_chart/fl_chart.dart' ;
20
20
import 'package:flutter/material.dart' ;
21
+ import 'package:flutter_gen/gen_l10n/app_localizations.dart' ;
21
22
import 'package:intl/intl.dart' ;
22
23
import 'package:wger/helpers/charts.dart' ;
23
24
25
+ class MeasurementOverallChangeWidget extends StatelessWidget {
26
+ final MeasurementChartEntry _first;
27
+ final MeasurementChartEntry _last;
28
+ final String _unit;
29
+ const MeasurementOverallChangeWidget (this ._first, this ._last, this ._unit);
30
+
31
+ @override
32
+ Widget build (BuildContext context) {
33
+ return Text (
34
+ 'overall change ${(_last .value - _first .value ).toStringAsFixed (1 )} $_unit ' ,
35
+ );
36
+ }
37
+ }
38
+
39
+ String weightUnit (bool isMetric, BuildContext context) {
40
+ return isMetric ? AppLocalizations .of (context).kg : AppLocalizations .of (context).lb;
41
+ }
42
+
24
43
class MeasurementChartWidgetFl extends StatefulWidget {
25
44
final List <MeasurementChartEntry > _entries;
26
- final String unit;
45
+ final List <MeasurementChartEntry >? avgs;
46
+ final String _unit;
27
47
28
- const MeasurementChartWidgetFl (this ._entries, { this .unit = 'kg' });
48
+ const MeasurementChartWidgetFl (this ._entries, this ._unit, { this .avgs });
29
49
30
50
@override
31
51
State <MeasurementChartWidgetFl > createState () => _MeasurementChartWidgetFlState ();
@@ -37,12 +57,7 @@ class _MeasurementChartWidgetFlState extends State<MeasurementChartWidgetFl> {
37
57
return AspectRatio (
38
58
aspectRatio: 1.70 ,
39
59
child: Padding (
40
- padding: const EdgeInsets .only (
41
- right: 18 ,
42
- left: 12 ,
43
- top: 24 ,
44
- bottom: 12 ,
45
- ),
60
+ padding: const EdgeInsets .all (4 ),
46
61
child: LineChart (mainData ()),
47
62
),
48
63
);
@@ -53,8 +68,8 @@ class _MeasurementChartWidgetFlState extends State<MeasurementChartWidgetFl> {
53
68
touchTooltipData: LineTouchTooltipData (getTooltipItems: (touchedSpots) {
54
69
return touchedSpots.map ((touchedSpot) {
55
70
return LineTooltipItem (
56
- '${touchedSpot .y } kg ' ,
57
- const TextStyle (color: Colors .red , fontWeight: FontWeight .bold),
71
+ '${touchedSpot .y . toStringAsFixed ( 1 )} ${ widget . _unit } ' ,
72
+ TextStyle (color: touchedSpot.bar.color , fontWeight: FontWeight .bold),
58
73
);
59
74
}).toList ();
60
75
}),
@@ -67,13 +82,13 @@ class _MeasurementChartWidgetFlState extends State<MeasurementChartWidgetFl> {
67
82
gridData: FlGridData (
68
83
show: true ,
69
84
drawVerticalLine: true ,
70
- //horizontalInterval: 1,
71
- //verticalInterval: interval ,
85
+ // horizontalInterval: 1,
86
+ // verticalInterval: 1 ,
72
87
getDrawingHorizontalLine: (value) {
73
- return const FlLine (color: Colors .grey , strokeWidth: 1 );
88
+ return FlLine (color: Theme . of (context).colorScheme.primaryContainer , strokeWidth: 1 );
74
89
},
75
90
getDrawingVerticalLine: (value) {
76
- return const FlLine (color: Colors .grey , strokeWidth: 1 );
91
+ return FlLine (color: Theme . of (context).colorScheme.primaryContainer , strokeWidth: 1 );
77
92
},
78
93
),
79
94
titlesData: FlTitlesData (
@@ -94,8 +109,15 @@ class _MeasurementChartWidgetFlState extends State<MeasurementChartWidgetFl> {
94
109
return const Text ('' );
95
110
}
96
111
final DateTime date = DateTime .fromMillisecondsSinceEpoch (value.toInt ());
112
+ // if we go across years, show years in the ticks. otherwise leave them out
113
+ if (DateTime .fromMillisecondsSinceEpoch (meta.min.toInt ()).year !=
114
+ DateTime .fromMillisecondsSinceEpoch (meta.max.toInt ()).year) {
115
+ return Text (
116
+ DateFormat .yMd (Localizations .localeOf (context).languageCode).format (date),
117
+ );
118
+ }
97
119
return Text (
98
- DateFormat .yMd (Localizations .localeOf (context).languageCode).format (date),
120
+ DateFormat .Md (Localizations .localeOf (context).languageCode).format (date),
99
121
);
100
122
},
101
123
interval: widget._entries.isNotEmpty
@@ -111,29 +133,47 @@ class _MeasurementChartWidgetFlState extends State<MeasurementChartWidgetFl> {
111
133
showTitles: true ,
112
134
reservedSize: 65 ,
113
135
getTitlesWidget: (value, meta) {
114
- return Text ('$value ${widget .unit }' );
136
+ // Don't show the first and last entries, otherwise they'll overlap with the
137
+ // calculated interval
138
+ if (value == meta.min || value == meta.max) {
139
+ return const Text ('' );
140
+ }
141
+ return Text ('$value ${widget ._unit }' );
115
142
},
116
143
),
117
144
),
118
145
),
119
146
borderData: FlBorderData (
120
147
show: true ,
121
- border: Border .all (color: const Color ( 0xff37434d ) ),
148
+ border: Border .all (color: Theme . of (context).colorScheme.primaryContainer ),
122
149
),
123
150
lineBarsData: [
124
151
LineChartBarData (
125
- spots: [
126
- ...widget._entries .map ((e) => FlSpot (
127
- e.date.millisecondsSinceEpoch.toDouble (),
128
- e.value.toDouble (),
129
- )),
130
- ] ,
152
+ spots: widget._entries
153
+ .map ((e) => FlSpot (
154
+ e.date.millisecondsSinceEpoch.toDouble (),
155
+ e.value.toDouble (),
156
+ ))
157
+ . toList () ,
131
158
isCurved: false ,
132
- color: Theme .of (context).colorScheme.secondary ,
133
- barWidth: 2 ,
159
+ color: Theme .of (context).colorScheme.primary ,
160
+ barWidth: 0 ,
134
161
isStrokeCapRound: true ,
135
162
dotData: const FlDotData (show: true ),
136
163
),
164
+ if (widget.avgs != null )
165
+ LineChartBarData (
166
+ spots: widget.avgs!
167
+ .map ((e) => FlSpot (
168
+ e.date.millisecondsSinceEpoch.toDouble (),
169
+ e.value.toDouble (),
170
+ ))
171
+ .toList (),
172
+ isCurved: false ,
173
+ color: Theme .of (context).colorScheme.tertiary,
174
+ barWidth: 1 ,
175
+ dotData: const FlDotData (show: false ),
176
+ ),
137
177
],
138
178
);
139
179
}
@@ -146,6 +186,34 @@ class MeasurementChartEntry {
146
186
MeasurementChartEntry (this .value, this .date);
147
187
}
148
188
189
+ // for each point, return the average of all the points in the 7 days preceeding it
190
+ List <MeasurementChartEntry > moving7dAverage (List <MeasurementChartEntry > vals) {
191
+ var start = 0 ;
192
+ var end = 0 ;
193
+ final List <MeasurementChartEntry > out = < MeasurementChartEntry > [];
194
+
195
+ // first make sure our list is in ascending order
196
+ vals.sort ((a, b) => a.date.compareTo (b.date));
197
+
198
+ while (end < vals.length) {
199
+ // since users can log measurements several days, or minutes apart,
200
+ // we can't make assumptions. We have to manually advance 'start'
201
+ // such that it is always the first point within our desired range.
202
+ // posibly start == end (when there is only one point in the range)
203
+ final intervalStart = vals[end].date.subtract (const Duration (days: 7 ));
204
+ while (start < end && vals[start].date.isBefore (intervalStart)) {
205
+ start++ ;
206
+ }
207
+
208
+ final sub = vals.sublist (start, end + 1 ).map ((e) => e.value);
209
+ final sum = sub.reduce ((val, el) => val + el);
210
+ out.add (MeasurementChartEntry (sum / sub.length, vals[end].date));
211
+
212
+ end++ ;
213
+ }
214
+ return out;
215
+ }
216
+
149
217
class Indicator extends StatelessWidget {
150
218
const Indicator ({
151
219
super .key,
0 commit comments