Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 129 additions & 39 deletions extras/log_file_client/lib/components/graph_view.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:log_file_client/components/chart_series_selector.dart';
import 'package:log_file_client/components/time_range_selector.dart';
import 'package:log_file_client/utils/http_client.dart';
Expand All @@ -9,10 +8,12 @@ import 'package:syncfusion_flutter_charts/charts.dart';
class GraphView extends StatefulWidget {
const GraphView({
required this.logData,
this.now,
super.key,
});

final List<LogDataLine> logData;
final DateTime? now;

@override
State<GraphView> createState() => _GraphViewState();
Expand All @@ -21,19 +22,24 @@ class GraphView extends StatefulWidget {
class _GraphViewState extends State<GraphView> {
bool _showPH = true;
bool _showTemp = true;
late final DateTime now;
late final DateTimeRange avaliableTimeRange;
late List<int> _displayedTimeRange = [
max(widget.logData.length - 1400, 0),
widget.logData.length,
]; // 1 day
late DateTimeRange displayedTimeRange;
late List<int> _displayedTimeRangeIndices;
late DateTimeIntervalType timeIntervalType;
late double timeInterval;
late DateFormat timeFormat;

@override
void initState() {
super.initState();
now = widget.now ?? DateTime.now();
avaliableTimeRange = DateTimeRange(
start: widget.logData.first.time,
end: widget.logData.last.time,
);
toggleTimeRange(2, null);
calculateTimeAxisFormat();
}

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

void toggleTimeRange(int index, DateTimeRange? customRange) {
final ranges = [
360, // 6 hours
720, // 12 hours
1440, // 1 day
4320, // 3 days
10080, // 7 days
43200, // 30 days
9999999, // Max
// index: 0-5 are predefined ranges, 6 is max, 7 is custom
final List<Duration> durations = [
Duration(hours: 6),
Duration(hours: 12),
Duration(days: 1),
Duration(days: 3),
Duration(days: 7),
Duration(days: 30),
];

late DateTimeRange timeRange;
if (index < 6) {
timeRange = DateTimeRange(
start: now.subtract(durations[index]),
end: now,
);
} else if (index == 6) {
timeRange = avaliableTimeRange;
} else {
timeRange = customRange!;
}

setState(() {
if (index < 7) {
_displayedTimeRange = [
max(widget.logData.length - ranges[index], 0),
widget.logData.length,
];
} else if (index == 7) {
final int endOffset =
avaliableTimeRange.end.difference(customRange!.end).inMinutes;
final int endIndex = widget.logData.length - endOffset;
final int startIndex = endIndex - customRange.duration.inMinutes;
_displayedTimeRange = [startIndex, endIndex];
}
displayedTimeRange = timeRange;
_displayedTimeRangeIndices = calculateTimeRange(timeRange);
calculateTimeAxisFormat();
});
}

List<int> calculateTimeRange(DateTimeRange range) {
int startIndex = 0;
int endIndex = widget.logData.length;
for (int i = widget.logData.length - 1; i >= 0; i--) {
if (widget.logData[i].time.compareTo(range.start) < 0) {
startIndex = i + 1;
break;
}
}
for (int i = widget.logData.length - 1; i >= 0; i--) {
if (widget.logData[i].time.compareTo(range.end) > 0) {
endIndex = i + 1;
break;
}
}
return [startIndex, endIndex];
}

void calculateTimeAxisFormat() {
timeInterval = 1;

if (displayedTimeRange.duration <= Duration(hours: 24)) {
timeIntervalType = DateTimeIntervalType.hours;
timeFormat = DateFormat('h a');
} else if (displayedTimeRange.duration <= Duration(days: 3)) {
timeIntervalType = DateTimeIntervalType.days;
timeFormat = DateFormat('h a\nMMM d');
timeInterval = 0.5;
} else {
timeIntervalType = DateTimeIntervalType.days;
timeFormat = DateFormat('MMM d');
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
Expand All @@ -83,8 +126,8 @@ class _GraphViewState extends State<GraphView> {
_topRow(widget.logData),
_graph(
widget.logData.sublist(
_displayedTimeRange[0],
_displayedTimeRange[1],
_displayedTimeRangeIndices[0],
_displayedTimeRangeIndices[1],
),
),
],
Expand Down Expand Up @@ -122,29 +165,30 @@ class _GraphViewState extends State<GraphView> {
}

Widget _graph(List<LogDataLine> logData) {
final List<Map<String, dynamic>> trackballData = [];
final trackballBehavior = TrackballBehavior(
enable: true,
tooltipDisplayMode: TrackballDisplayMode.groupAllPoints,
activationMode: ActivationMode.singleTap,
markerSettings: TrackballMarkerSettings(
markerVisibility: TrackballVisibilityMode.visible,
),
tooltipSettings: InteractiveTooltip(
enable: true,
format: 'series.name : point.y',
),
activationMode: ActivationMode.singleTap,
lineColor: Colors.grey.shade600,
lineWidth: 1.5,
lineDashArray: [2, 2],
// builder: _trackballBuilder,
);

return Expanded(
child: SfCartesianChart(
backgroundColor: Colors.white,
primaryXAxis: DateTimeAxis(
title: AxisTitle(text: 'Time'),
intervalType: DateTimeIntervalType.hours,
interval: 1,
intervalType: timeIntervalType,
interval: timeInterval,
dateFormat: timeFormat,
minimum: displayedTimeRange.start,
maximum: displayedTimeRange.end,
),
primaryYAxis: NumericAxis(
name: 'pHAxis',
Expand All @@ -160,14 +204,52 @@ class _GraphViewState extends State<GraphView> {
),
],
trackballBehavior: trackballBehavior,
onTrackballPositionChanging: (TrackballArgs details) {
// Clear previously stored data on each update.
trackballData.clear();

// Store the details for the current trackball position.
if (details.chartPointInfo.series != null &&
details.chartPointInfo.chartPoint != null) {
final String seriesName = details.chartPointInfo.series!.name;
final DateTime date =
details.chartPointInfo.chartPoint!.x as DateTime;
final double yValue =
details.chartPointInfo.chartPoint!.y as double;

// Add this series' details to the trackballData list.
trackballData.add({
'seriesName': seriesName,
'yValue': yValue,
'date': date,
});

// Construct the tooltip dynamically from the stored data.
String tooltipText = '';
String seriesText = '';
for (final data in trackballData) {
seriesText += '${data['seriesName']} : ${data['yValue']}';
}

// Combine the series data into a compact format
tooltipText += seriesText;

// Update the trackball tooltip information
details.chartPointInfo.label = tooltipText;
details.chartPointInfo.header =
DateFormat('MMM d hh:mm a').format(date);
}
},
series: _chartSeries(logData),
),
);
}

List<CartesianSeries> _chartSeries(List<LogDataLine> logData) {
final MarkerSettings markerSettings = MarkerSettings(height: 2, width: 2);

return <CartesianSeries>[
LineSeries<LogDataLine, DateTime>(
ScatterSeries<LogDataLine, DateTime>(
legendItemText: 'pH',
name: 'pH',
dataSource: logData,
Expand All @@ -176,8 +258,10 @@ class _GraphViewState extends State<GraphView> {
color: _showPH ? Colors.green : Colors.transparent,
yAxisName: 'pHAxis',
animationDuration: 0,
markerSettings: markerSettings,
enableTrackball: _showPH,
),
LineSeries<LogDataLine, DateTime>(
ScatterSeries<LogDataLine, DateTime>(
legendItemText: 'pH setpoint',
name: 'pH setpoint',
dataSource: logData,
Expand All @@ -186,8 +270,10 @@ class _GraphViewState extends State<GraphView> {
color: _showPH ? Colors.green.shade800 : Colors.transparent,
yAxisName: 'pHAxis',
animationDuration: 0,
markerSettings: markerSettings,
enableTrackball: _showPH,
),
LineSeries<LogDataLine, DateTime>(
ScatterSeries<LogDataLine, DateTime>(
legendItemText: 'temp',
name: 'temp',
dataSource: logData,
Expand All @@ -196,8 +282,10 @@ class _GraphViewState extends State<GraphView> {
color: _showTemp ? Colors.blue : Colors.transparent,
yAxisName: 'TemperatureAxis',
animationDuration: 0,
markerSettings: markerSettings,
enableTrackball: _showTemp,
),
LineSeries<LogDataLine, DateTime>(
ScatterSeries<LogDataLine, DateTime>(
legendItemText: 'temp setpoint',
name: 'temp setpoint',
dataSource: logData,
Expand All @@ -206,6 +294,8 @@ class _GraphViewState extends State<GraphView> {
color: _showTemp ? Colors.blue.shade800 : Colors.transparent,
yAxisName: 'TemperatureAxis',
animationDuration: 0,
markerSettings: markerSettings,
enableTrackball: _showTemp,
),
];
}
Expand Down
11 changes: 7 additions & 4 deletions extras/log_file_client/lib/components/page_header.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ class PageHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final double fontSize = screenWidth >= 500 ? 60 : 40;
final double sideMargins = screenWidth >= 500 ? 135 : 40;
final double topMargin = screenWidth >= 500 ? 40 : 30;

return Container(
margin: EdgeInsets.only(
top: 40,
left: 135,
right: 135,
top: topMargin,
left: sideMargins,
right: sideMargins,
),
padding: EdgeInsets.only(bottom: 16),
width: screenWidth,
Expand All @@ -27,7 +30,7 @@ class PageHeader extends StatelessWidget {
child: Text(
text,
style: TextStyle(
fontSize: 60,
fontSize: fontSize,
letterSpacing: -2,
color: const Color(0xFF0C2D48),
),
Expand Down
16 changes: 8 additions & 8 deletions extras/log_file_client/lib/components/tank_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class TankCard extends StatelessWidget {
onTap: onTap,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final double cardWidth = constraints.maxWidth * 0.93;
final double cardWidth = constraints.maxWidth;
final double titleFontSize = 20;
final double tankInfoFontSize = 16;
final double tankInfoHeaderFontSize = 14;
Expand Down Expand Up @@ -101,7 +101,7 @@ class TankCard extends StatelessWidget {
AsyncSnapshot<TankSnapshot>? snapshot,
) {
return SizedBox(
height: cardWidth * 0.6,
height: cardWidth * 0.53,
child: ClipRRect(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
Expand Down Expand Up @@ -241,18 +241,18 @@ class TankCard extends StatelessWidget {
String textData = 'mock';
if (mode == TankInfoMode.current) {
if (type == TankInfoType.pH) {
textData = 'pH ${snapshot?.data!.pH ?? 'mock'}';
textData = 'pH ${snapshot?.data!.pH ?? '----'}';
} else {
textData = '${snapshot?.data!.temperature ?? 'mock'}°C';
textData = '${snapshot?.data!.temperature ?? '----'}°C';
}
} else if (mode == TankInfoMode.range) {
if (type == TankInfoType.pH) {
final min = snapshot?.data!.minPH ?? 'm.k';
final max = snapshot?.data!.maxPH ?? 'm.k';
final min = snapshot?.data!.minPH ?? '---';
final max = snapshot?.data!.maxPH ?? '---';
textData = 'pH $min - $max';
} else {
final min = snapshot?.data!.minTemp ?? 'mk';
final max = snapshot?.data!.maxTemp ?? 'mk';
final min = snapshot?.data!.minTemp ?? '--';
final max = snapshot?.data!.maxTemp ?? '--';
textData = '$min - $max°C';
}
}
Expand Down
Loading