import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'dart:ui' as ui; import 'package:vbvs_app/common/util/MyUtils.dart'; class TimeLineChart extends StatelessWidget { final List points; final double yMin; final double yMax; final int startTime; final int endTime; final double width; final double height; const TimeLineChart({ super.key, required this.points, required this.yMin, required this.yMax, required this.startTime, required this.endTime, this.width = 400, this.height = 300, }); @override Widget build(BuildContext context) { return CustomPaint( size: Size(width, height), painter: _TimeLineChartPainter( points: points, yMin: yMin, yMax: yMax, startTime: startTime, endTime: endTime, ), ); } } class DataPoint { final int timestamp; final double value; DataPoint(this.timestamp, this.value); } class _TimeLineChartPainter extends CustomPainter { final List points; final double yMin; final double yMax; final int startTime; final int endTime; _TimeLineChartPainter({ required this.points, required this.yMin, required this.yMax, required this.startTime, required this.endTime, }); @override void paint(Canvas canvas, Size size) { _drawYAxis(canvas, size); _drawXAxis(canvas, size); _drawLine(canvas, size); } void _drawXAxis(Canvas canvas, Size size) { const margin = 40.0; final paint = Paint()..color = Colors.black; final textStyle = const TextStyle(color: Colors.black, fontSize: 12); // Draw X axis line canvas.drawLine( Offset(margin, size.height - margin), Offset(size.width - margin, size.height - margin), paint, ); // Generate time ticks final timeFormatStartEnd = DateFormat('HH:mm'); final timeFormatMiddle = DateFormat('h'); final startDateTime = DateTime.fromMillisecondsSinceEpoch(startTime); final endDateTime = DateTime.fromMillisecondsSinceEpoch(endTime); List hourTicks = []; DateTime current = DateTime( startDateTime.year, startDateTime.month, startDateTime.day, startDateTime.hour, ).add(const Duration(hours: 1)); while (current.isBefore(endDateTime)) { if (current.isAfter(startDateTime)) { hourTicks.add(current); } current = current.add(const Duration(hours: 1)); } void drawTick(DateTime time, bool isEdge) { final x = margin + ((time.millisecondsSinceEpoch - startTime) / (endTime - startTime)) * (size.width - 2 * margin); final text = isEdge ? timeFormatStartEnd.format(time) : timeFormatMiddle.format(time); _drawText( canvas, text, Offset(x, size.height - margin + 20), TextAlign.center, ); } drawTick(startDateTime, true); drawTick(endDateTime, true); for (var tick in hourTicks) { drawTick(tick, false); } } void _drawYAxis(Canvas canvas, Size size) { const margin = 40.0; final midValue = (yMax + yMin) / 2; // 计算三条虚线之间的垂直间距 final lineSpacing = (size.height - 2 * margin) / 3; // 让三条线之间的间距相等 // 新增的 y=0 实线的垂直位置 final zeroLinePosition = margin + lineSpacing * 3; // 确保 y=0 位于三条虚线下方 void drawLine(double value, Color color, {bool isDashed = false, bool isSolid = false}) { final y = (value - yMax) / (yMin - yMax) * (size.height - 2 * margin) + margin; final path = Path(); path.moveTo(margin, y); path.lineTo(size.width - margin, y); final paint = Paint() ..color = color ..strokeWidth = (color != Colors.grey) ? 2 : 1 ..style = PaintingStyle.stroke; if (isDashed) { Path dashedPath = _createDashedPath(path, dashWidth: 5, dashSpace: 5); canvas.drawPath(dashedPath, paint); } else if (isSolid) { // 对于实线,直接绘制 canvas.drawPath(path, paint); } else { // 默认使用虚线绘制 canvas.drawPath(path, paint); } } // 绘制 y=0 的灰色实线,并将其放置在三条虚线的下方 if (yMin < 0 && yMax > 0) { drawLine(0, Colors.grey, isSolid: true); // 灰色实线绘制 y=0 线 } // 绘制最小值、中间值、最大值的虚线 drawLine(yMin, themeController.currentColor.sc9, isDashed: true); drawLine(midValue, themeController.currentColor.sc4, isDashed: true); drawLine(yMax, themeController.currentColor.sc9, isDashed: true); } Path _createDashedPath(Path path, {required double dashWidth, required double dashSpace}) { final Path dashedPath = Path(); final ui.PathMetrics metrics = path.computeMetrics(); for (ui.PathMetric metric in metrics) { double distance = 0; while (distance < metric.length) { dashedPath.addPath( metric.extractPath(distance, distance + dashWidth), Offset.zero, ); distance += dashWidth + dashSpace; } } return dashedPath; } void _drawLine(Canvas canvas, Size size) { const margin = 40.0; final sortedPoints = points ..sort((a, b) => a.timestamp.compareTo(b.timestamp)); Path? currentPath; Paint currentPaint = _createPaint(Colors.green); for (int i = 0; i < sortedPoints.length - 1; i++) { final p1 = sortedPoints[i]; final p2 = sortedPoints[i + 1]; final x1 = margin + ((p1.timestamp - startTime) / (endTime - startTime)) * (size.width - 2 * margin); final y1 = margin + (1 - (p1.value - yMin) / (yMax - yMin)) * (size.height - 2 * margin); final x2 = margin + ((p2.timestamp - startTime) / (endTime - startTime)) * (size.width - 2 * margin); final y2 = margin + (1 - (p2.value - yMin) / (yMax - yMin)) * (size.height - 2 * margin); final shouldBeGreen = p1.value >= yMin && p1.value <= yMax && p2.value >= yMin && p2.value <= yMax; // 根据当前线段的状态来决定是否切换颜色和虚线状态 if (shouldBeGreen != (currentPaint.color == Colors.green)) { if (currentPath != null) { canvas.drawPath(currentPath, currentPaint); } currentPath = Path(); currentPaint = _createPaint(shouldBeGreen ? Colors.green : Colors.red); } currentPath ??= Path(); if (i == 0) currentPath.moveTo(x1, y1); currentPath.lineTo(x2, y2); } // 绘制剩余路径 if (currentPath != null) { if (currentPaint.color == Colors.red) { // 如果是红色线,绘制虚线 final dashedPath = _createDashedPath(currentPath, dashWidth: 5, dashSpace: 5); canvas.drawPath(dashedPath, currentPaint); } else { canvas.drawPath(currentPath, currentPaint); } } } Paint _createPaint(Color color) => Paint() ..color = color ..strokeWidth = 2 ..style = PaintingStyle.stroke; void _drawText(Canvas canvas, String text, Offset offset, TextAlign align) { final textPainter = TextPainter( text: TextSpan( text: text, style: const TextStyle(color: Colors.black, fontSize: 12), ), textDirection: ui.TextDirection.ltr, )..layout(); final centeredOffset = offset.translate( -textPainter.width / 2, -textPainter.height / 2, ); textPainter.paint(canvas, centeredOffset); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; }