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