Skip to content

Commit 5317dc9

Browse files
authored
* Fixed loading issues that appeared in deployment * Dates are displayed * Changed line plot to scatter plot - to not show data where it doesn't exist * Hid tooltip info about hidden ph/temp * Improved layout on mobile * Server API looks at timestamps instead of just counting lines * If no data for past 12 hours, no thumbnail is displayed * "now" is a parameter for easier testing * Formatting * Date axis format changes depending on time range * Trackball date formatting is independent of axis formatting * Main graph view calculates time intervals based on datetime, not line number * Trackball works with series switched off
1 parent a15bcbe commit 5317dc9

14 files changed

Lines changed: 392 additions & 118 deletions

File tree

extras/log_file_client/lib/components/graph_view.dart

Lines changed: 129 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import 'dart:math';
2-
31
import 'package:flutter/material.dart';
2+
import 'package:intl/intl.dart';
43
import 'package:log_file_client/components/chart_series_selector.dart';
54
import 'package:log_file_client/components/time_range_selector.dart';
65
import 'package:log_file_client/utils/http_client.dart';
@@ -9,10 +8,12 @@ import 'package:syncfusion_flutter_charts/charts.dart';
98
class GraphView extends StatefulWidget {
109
const GraphView({
1110
required this.logData,
11+
this.now,
1212
super.key,
1313
});
1414

1515
final List<LogDataLine> logData;
16+
final DateTime? now;
1617

1718
@override
1819
State<GraphView> createState() => _GraphViewState();
@@ -21,19 +22,24 @@ class GraphView extends StatefulWidget {
2122
class _GraphViewState extends State<GraphView> {
2223
bool _showPH = true;
2324
bool _showTemp = true;
25+
late final DateTime now;
2426
late final DateTimeRange avaliableTimeRange;
25-
late List<int> _displayedTimeRange = [
26-
max(widget.logData.length - 1400, 0),
27-
widget.logData.length,
28-
]; // 1 day
27+
late DateTimeRange displayedTimeRange;
28+
late List<int> _displayedTimeRangeIndices;
29+
late DateTimeIntervalType timeIntervalType;
30+
late double timeInterval;
31+
late DateFormat timeFormat;
2932

3033
@override
3134
void initState() {
3235
super.initState();
36+
now = widget.now ?? DateTime.now();
3337
avaliableTimeRange = DateTimeRange(
3438
start: widget.logData.first.time,
3539
end: widget.logData.last.time,
3640
);
41+
toggleTimeRange(2, null);
42+
calculateTimeAxisFormat();
3743
}
3844

3945
void toggleSeriesView(int index) {
@@ -47,32 +53,69 @@ class _GraphViewState extends State<GraphView> {
4753
}
4854

4955
void toggleTimeRange(int index, DateTimeRange? customRange) {
50-
final ranges = [
51-
360, // 6 hours
52-
720, // 12 hours
53-
1440, // 1 day
54-
4320, // 3 days
55-
10080, // 7 days
56-
43200, // 30 days
57-
9999999, // Max
56+
// index: 0-5 are predefined ranges, 6 is max, 7 is custom
57+
final List<Duration> durations = [
58+
Duration(hours: 6),
59+
Duration(hours: 12),
60+
Duration(days: 1),
61+
Duration(days: 3),
62+
Duration(days: 7),
63+
Duration(days: 30),
5864
];
5965

66+
late DateTimeRange timeRange;
67+
if (index < 6) {
68+
timeRange = DateTimeRange(
69+
start: now.subtract(durations[index]),
70+
end: now,
71+
);
72+
} else if (index == 6) {
73+
timeRange = avaliableTimeRange;
74+
} else {
75+
timeRange = customRange!;
76+
}
77+
6078
setState(() {
61-
if (index < 7) {
62-
_displayedTimeRange = [
63-
max(widget.logData.length - ranges[index], 0),
64-
widget.logData.length,
65-
];
66-
} else if (index == 7) {
67-
final int endOffset =
68-
avaliableTimeRange.end.difference(customRange!.end).inMinutes;
69-
final int endIndex = widget.logData.length - endOffset;
70-
final int startIndex = endIndex - customRange.duration.inMinutes;
71-
_displayedTimeRange = [startIndex, endIndex];
72-
}
79+
displayedTimeRange = timeRange;
80+
_displayedTimeRangeIndices = calculateTimeRange(timeRange);
81+
calculateTimeAxisFormat();
7382
});
7483
}
7584

85+
List<int> calculateTimeRange(DateTimeRange range) {
86+
int startIndex = 0;
87+
int endIndex = widget.logData.length;
88+
for (int i = widget.logData.length - 1; i >= 0; i--) {
89+
if (widget.logData[i].time.compareTo(range.start) < 0) {
90+
startIndex = i + 1;
91+
break;
92+
}
93+
}
94+
for (int i = widget.logData.length - 1; i >= 0; i--) {
95+
if (widget.logData[i].time.compareTo(range.end) > 0) {
96+
endIndex = i + 1;
97+
break;
98+
}
99+
}
100+
return [startIndex, endIndex];
101+
}
102+
103+
void calculateTimeAxisFormat() {
104+
timeInterval = 1;
105+
106+
if (displayedTimeRange.duration <= Duration(hours: 24)) {
107+
timeIntervalType = DateTimeIntervalType.hours;
108+
timeFormat = DateFormat('h a');
109+
} else if (displayedTimeRange.duration <= Duration(days: 3)) {
110+
timeIntervalType = DateTimeIntervalType.days;
111+
timeFormat = DateFormat('h a\nMMM d');
112+
timeInterval = 0.5;
113+
} else {
114+
timeIntervalType = DateTimeIntervalType.days;
115+
timeFormat = DateFormat('MMM d');
116+
}
117+
}
118+
76119
@override
77120
Widget build(BuildContext context) {
78121
return Scaffold(
@@ -83,8 +126,8 @@ class _GraphViewState extends State<GraphView> {
83126
_topRow(widget.logData),
84127
_graph(
85128
widget.logData.sublist(
86-
_displayedTimeRange[0],
87-
_displayedTimeRange[1],
129+
_displayedTimeRangeIndices[0],
130+
_displayedTimeRangeIndices[1],
88131
),
89132
),
90133
],
@@ -122,29 +165,30 @@ class _GraphViewState extends State<GraphView> {
122165
}
123166

124167
Widget _graph(List<LogDataLine> logData) {
168+
final List<Map<String, dynamic>> trackballData = [];
125169
final trackballBehavior = TrackballBehavior(
126170
enable: true,
127171
tooltipDisplayMode: TrackballDisplayMode.groupAllPoints,
172+
activationMode: ActivationMode.singleTap,
128173
markerSettings: TrackballMarkerSettings(
129174
markerVisibility: TrackballVisibilityMode.visible,
130175
),
131-
tooltipSettings: InteractiveTooltip(
132-
enable: true,
133-
format: 'series.name : point.y',
134-
),
135-
activationMode: ActivationMode.singleTap,
136176
lineColor: Colors.grey.shade600,
137177
lineWidth: 1.5,
138178
lineDashArray: [2, 2],
179+
// builder: _trackballBuilder,
139180
);
140181

141182
return Expanded(
142183
child: SfCartesianChart(
143184
backgroundColor: Colors.white,
144185
primaryXAxis: DateTimeAxis(
145186
title: AxisTitle(text: 'Time'),
146-
intervalType: DateTimeIntervalType.hours,
147-
interval: 1,
187+
intervalType: timeIntervalType,
188+
interval: timeInterval,
189+
dateFormat: timeFormat,
190+
minimum: displayedTimeRange.start,
191+
maximum: displayedTimeRange.end,
148192
),
149193
primaryYAxis: NumericAxis(
150194
name: 'pHAxis',
@@ -160,14 +204,52 @@ class _GraphViewState extends State<GraphView> {
160204
),
161205
],
162206
trackballBehavior: trackballBehavior,
207+
onTrackballPositionChanging: (TrackballArgs details) {
208+
// Clear previously stored data on each update.
209+
trackballData.clear();
210+
211+
// Store the details for the current trackball position.
212+
if (details.chartPointInfo.series != null &&
213+
details.chartPointInfo.chartPoint != null) {
214+
final String seriesName = details.chartPointInfo.series!.name;
215+
final DateTime date =
216+
details.chartPointInfo.chartPoint!.x as DateTime;
217+
final double yValue =
218+
details.chartPointInfo.chartPoint!.y as double;
219+
220+
// Add this series' details to the trackballData list.
221+
trackballData.add({
222+
'seriesName': seriesName,
223+
'yValue': yValue,
224+
'date': date,
225+
});
226+
227+
// Construct the tooltip dynamically from the stored data.
228+
String tooltipText = '';
229+
String seriesText = '';
230+
for (final data in trackballData) {
231+
seriesText += '${data['seriesName']} : ${data['yValue']}';
232+
}
233+
234+
// Combine the series data into a compact format
235+
tooltipText += seriesText;
236+
237+
// Update the trackball tooltip information
238+
details.chartPointInfo.label = tooltipText;
239+
details.chartPointInfo.header =
240+
DateFormat('MMM d hh:mm a').format(date);
241+
}
242+
},
163243
series: _chartSeries(logData),
164244
),
165245
);
166246
}
167247

168248
List<CartesianSeries> _chartSeries(List<LogDataLine> logData) {
249+
final MarkerSettings markerSettings = MarkerSettings(height: 2, width: 2);
250+
169251
return <CartesianSeries>[
170-
LineSeries<LogDataLine, DateTime>(
252+
ScatterSeries<LogDataLine, DateTime>(
171253
legendItemText: 'pH',
172254
name: 'pH',
173255
dataSource: logData,
@@ -176,8 +258,10 @@ class _GraphViewState extends State<GraphView> {
176258
color: _showPH ? Colors.green : Colors.transparent,
177259
yAxisName: 'pHAxis',
178260
animationDuration: 0,
261+
markerSettings: markerSettings,
262+
enableTrackball: _showPH,
179263
),
180-
LineSeries<LogDataLine, DateTime>(
264+
ScatterSeries<LogDataLine, DateTime>(
181265
legendItemText: 'pH setpoint',
182266
name: 'pH setpoint',
183267
dataSource: logData,
@@ -186,8 +270,10 @@ class _GraphViewState extends State<GraphView> {
186270
color: _showPH ? Colors.green.shade800 : Colors.transparent,
187271
yAxisName: 'pHAxis',
188272
animationDuration: 0,
273+
markerSettings: markerSettings,
274+
enableTrackball: _showPH,
189275
),
190-
LineSeries<LogDataLine, DateTime>(
276+
ScatterSeries<LogDataLine, DateTime>(
191277
legendItemText: 'temp',
192278
name: 'temp',
193279
dataSource: logData,
@@ -196,8 +282,10 @@ class _GraphViewState extends State<GraphView> {
196282
color: _showTemp ? Colors.blue : Colors.transparent,
197283
yAxisName: 'TemperatureAxis',
198284
animationDuration: 0,
285+
markerSettings: markerSettings,
286+
enableTrackball: _showTemp,
199287
),
200-
LineSeries<LogDataLine, DateTime>(
288+
ScatterSeries<LogDataLine, DateTime>(
201289
legendItemText: 'temp setpoint',
202290
name: 'temp setpoint',
203291
dataSource: logData,
@@ -206,6 +294,8 @@ class _GraphViewState extends State<GraphView> {
206294
color: _showTemp ? Colors.blue.shade800 : Colors.transparent,
207295
yAxisName: 'TemperatureAxis',
208296
animationDuration: 0,
297+
markerSettings: markerSettings,
298+
enableTrackball: _showTemp,
209299
),
210300
];
211301
}

extras/log_file_client/lib/components/page_header.dart

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ class PageHeader extends StatelessWidget {
77
@override
88
Widget build(BuildContext context) {
99
final screenWidth = MediaQuery.of(context).size.width;
10+
final double fontSize = screenWidth >= 500 ? 60 : 40;
11+
final double sideMargins = screenWidth >= 500 ? 135 : 40;
12+
final double topMargin = screenWidth >= 500 ? 40 : 30;
1013

1114
return Container(
1215
margin: EdgeInsets.only(
13-
top: 40,
14-
left: 135,
15-
right: 135,
16+
top: topMargin,
17+
left: sideMargins,
18+
right: sideMargins,
1619
),
1720
padding: EdgeInsets.only(bottom: 16),
1821
width: screenWidth,
@@ -27,7 +30,7 @@ class PageHeader extends StatelessWidget {
2730
child: Text(
2831
text,
2932
style: TextStyle(
30-
fontSize: 60,
33+
fontSize: fontSize,
3134
letterSpacing: -2,
3235
color: const Color(0xFF0C2D48),
3336
),

extras/log_file_client/lib/components/tank_card.dart

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class TankCard extends StatelessWidget {
3333
onTap: onTap,
3434
child: LayoutBuilder(
3535
builder: (BuildContext context, BoxConstraints constraints) {
36-
final double cardWidth = constraints.maxWidth * 0.93;
36+
final double cardWidth = constraints.maxWidth;
3737
final double titleFontSize = 20;
3838
final double tankInfoFontSize = 16;
3939
final double tankInfoHeaderFontSize = 14;
@@ -101,7 +101,7 @@ class TankCard extends StatelessWidget {
101101
AsyncSnapshot<TankSnapshot>? snapshot,
102102
) {
103103
return SizedBox(
104-
height: cardWidth * 0.6,
104+
height: cardWidth * 0.53,
105105
child: ClipRRect(
106106
borderRadius: BorderRadius.vertical(
107107
top: Radius.circular(20),
@@ -241,18 +241,18 @@ class TankCard extends StatelessWidget {
241241
String textData = 'mock';
242242
if (mode == TankInfoMode.current) {
243243
if (type == TankInfoType.pH) {
244-
textData = 'pH ${snapshot?.data!.pH ?? 'mock'}';
244+
textData = 'pH ${snapshot?.data!.pH ?? '----'}';
245245
} else {
246-
textData = '${snapshot?.data!.temperature ?? 'mock'}°C';
246+
textData = '${snapshot?.data!.temperature ?? '----'}°C';
247247
}
248248
} else if (mode == TankInfoMode.range) {
249249
if (type == TankInfoType.pH) {
250-
final min = snapshot?.data!.minPH ?? 'm.k';
251-
final max = snapshot?.data!.maxPH ?? 'm.k';
250+
final min = snapshot?.data!.minPH ?? '---';
251+
final max = snapshot?.data!.maxPH ?? '---';
252252
textData = 'pH $min - $max';
253253
} else {
254-
final min = snapshot?.data!.minTemp ?? 'mk';
255-
final max = snapshot?.data!.maxTemp ?? 'mk';
254+
final min = snapshot?.data!.minTemp ?? '--';
255+
final max = snapshot?.data!.maxTemp ?? '--';
256256
textData = '$min - $max°C';
257257
}
258258
}

0 commit comments

Comments
 (0)