import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:vbvs_app/common/util/FitTool.dart'; import 'dart:math'; import 'package:vbvs_app/common/util/MyUtils.dart'; class TimeSeriesChart extends StatelessWidget { final int startTime; final int endTime; final double yMin; final double yMax; final List dataPoints; TimeSeriesChart({ required this.startTime, required this.endTime, required this.yMin, required this.yMax, required this.dataPoints, }); @override Widget build(BuildContext context) { final midValue = (yMax + yMin) / 2; final xLabels = _generateXLabels(); // Prepare spots and segments List spots = []; List lineColors = []; for (int i = 0; i < dataPoints.length; i++) { final point = dataPoints[i]; final xValue = _convertTimeToXValue(point.timestamp); final yValue = point.value; spots.add(FlSpot(xValue, yValue)); if (yValue >= yMin && yValue <= yMax) { lineColors.add(Colors.green); // Color for points within range } else { lineColors.add(Colors.red); // Color for points outside range } } return AspectRatio( aspectRatio: 2, child: LineChart( LineChartData( lineTouchData: LineTouchData( touchTooltipData: LineTouchTooltipData( getTooltipItems: (List touchedSpots) { return touchedSpots.map((spot) { final time = DateTime.fromMillisecondsSinceEpoch( _convertXValueToTime(spot.x), ); return LineTooltipItem( '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}\n${spot.y.toStringAsFixed(0)}', const TextStyle(color: Colors.black), ); }).toList(); }, ), ), gridData: FlGridData( show: true, drawVerticalLine: false, getDrawingHorizontalLine: (value) { if (value == 0) { return FlLine( color: themeController.currentColor.sc4, strokeWidth: 1, ); } else if (value == yMin) { return FlLine( color: themeController.currentColor.sc9, strokeWidth: 1, dashArray: [5, 5], ); } else if (value == yMax) { return FlLine( color: themeController.currentColor.sc9, strokeWidth: 1, dashArray: [5, 5], ); } else if (value == midValue) { return FlLine( color: themeController.currentColor.sc4, strokeWidth: 1, dashArray: [5, 5], ); } return FlLine( color: Colors.grey.withOpacity(0.1), strokeWidth: 1, ); }, ), titlesData: FlTitlesData( show: true, rightTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), ), topTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 30, getTitlesWidget: (value, meta) { final index = value.toInt(); if (index >= 0 && index < xLabels.length) { return Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( xLabels[index].label, style: TextStyle( fontSize: 10, color: Colors.grey, ), ), ); } return const Text(''); }, ), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { if (value == 0) { return Padding( padding: EdgeInsets.only(right: 14.rpx), child: Text( value.toStringAsFixed(0), style: TextStyle( fontSize: 18.rpx, color: themeController.currentColor.sc4, ), maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.right, ), ); } else if (value == yMin) { return Padding( padding: EdgeInsets.only(right: 14.rpx), child: Text( yMin.toStringAsFixed(0), style: TextStyle( fontSize: 18.rpx, color: themeController.currentColor.sc4, ), maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.right, ), ); } else if (value == midValue) { return Padding( padding: EdgeInsets.only(right: 14.rpx), child: Text( midValue.toStringAsFixed(0), style: TextStyle( fontSize: 18.rpx, color: themeController.currentColor.sc4, ), maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.right, ), ); } else if (value == yMax) { return Padding( padding: EdgeInsets.only(right: 14.rpx), child: Text( yMax.toStringAsFixed(0), style: TextStyle( fontSize: 18.rpx, color: themeController.currentColor.sc4, ), maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.right, ), ); } return const Text(''); }, reservedSize: 40, ), ), ), borderData: FlBorderData( show: false, border: Border.all(color: Colors.grey.withOpacity(0.3)), ), minX: 0, maxX: xLabels.length - 1, minY: min(0, yMin) - (yMax - yMin) * 0.2, maxY: yMax + (yMax - yMin) * 0.2, lineBarsData: [ LineChartBarData( spots: spots, isCurved: false, color: themeController.currentColor.sc2, barWidth: 2, isStrokeCapRound: true, dotData: FlDotData(show: false), // Disable dots belowBarData: BarAreaData(show: false), ), ], ), ), ); } List _generateXLabels() { final labels = []; final startDate = DateTime.fromMillisecondsSinceEpoch(startTime); final endDate = DateTime.fromMillisecondsSinceEpoch(endTime); labels.add(XLabel( time: startTime, label: '${startDate.hour.toString().padLeft(2, '0')}:${startDate.minute.toString().padLeft(2, '0')}', )); DateTime current = DateTime( startDate.year, startDate.month, startDate.day, startDate.hour + 1, ); while (current.isBefore(endDate)) { labels.add(XLabel( time: current.millisecondsSinceEpoch, label: current.hour.toString(), )); current = current.add(Duration(hours: 1)); } labels.add(XLabel( time: endTime, label: '${endDate.hour.toString().padLeft(2, '0')}:${endDate.minute.toString().padLeft(2, '0')}', )); return labels; } double _convertTimeToXValue(int timestamp) { final totalDuration = endTime - startTime; final pointDuration = timestamp - startTime; final xLabels = _generateXLabels(); return (pointDuration / totalDuration) * (xLabels.length - 1); } int _convertXValueToTime(double xValue) { final xLabels = _generateXLabels(); final totalDuration = endTime - startTime; final ratio = xValue / (xLabels.length - 1); return startTime + (totalDuration * ratio).round(); } } class TimeSeriesPoint { final int timestamp; final double value; TimeSeriesPoint(this.timestamp, this.value); } class XLabel { final int time; final String label; XLabel({required this.time, required this.label}); }