import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:vbvs_app/common/util/FitTool.dart'; import 'package:vbvs_app/common/util/MyUtils.dart'; import 'dart:ui' as ui; import 'dart:math'; class LineChartByRange extends StatelessWidget { final List> showLabel; final int startTime; final int endTime; const LineChartByRange({ Key? key, required this.showLabel, required this.startTime, required this.endTime, }) : super(key: key); @override Widget build(BuildContext context) { if (showLabel.isEmpty) return const SizedBox(); int maxTimes = showLabel.map((e) => e['times'] ?? 0).reduce((a, b) => a > b ? a : b); int yMax = (maxTimes / 10).ceil() * 10; if (yMax == 0) yMax = 10; DateTime minTime = DateTime.fromMillisecondsSinceEpoch(startTime); DateTime maxTime = DateTime.fromMillisecondsSinceEpoch(endTime); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( height: 500.rpx, child: CustomPaint( size: Size(double.infinity, 500.rpx), painter: _LineChartByRangePainter( data: showLabel, yMax: yMax, minTime: minTime, maxTime: maxTime, ), ), ), ], ); } } class _LineChartByRangePainter extends CustomPainter { final List> data; final int yMax; final DateTime minTime; final DateTime maxTime; _LineChartByRangePainter({ required this.data, required this.yMax, required this.minTime, required this.maxTime, }); @override void paint(Canvas canvas, Size size) { double padding = 20.rpx; double labelInset = 12.rpx; // X轴标签缩进距离 // 绘图X轴起止点,考虑内缩labelInset final double xStart = padding + labelInset; final double xEnd = size.width - padding - labelInset; final double chartWidth = xEnd - xStart; double chartHeight = size.height - 30.rpx; int totalDuration = maxTime.millisecondsSinceEpoch - minTime.millisecondsSinceEpoch; if (totalDuration <= 0) return; Paint linePaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 3.rpx ..color = stringToColor("#00C1AA") ..strokeCap = StrokeCap.round; Paint fillCirclePaint = Paint() ..style = PaintingStyle.fill ..color = stringToColor("#00C1AA"); // 1. 先绘制数据线段及起止点圆点 for (var item in data) { int start = item['startTime']; int end = item['endTime']; int times = item['times']; double startX = xStart + chartWidth * (start - minTime.millisecondsSinceEpoch) / totalDuration; double endX = xStart + chartWidth * (end - minTime.millisecondsSinceEpoch) / totalDuration; double y = chartHeight * (1 - times / yMax); // 画线段 canvas.drawLine(Offset(startX, y), Offset(endX, y), linePaint); // 画起点圆点 canvas.drawCircle(Offset(startX, y), 4.rpx, fillCirclePaint); // 画终点圆点 canvas.drawCircle(Offset(endX, y), 4.rpx, fillCirclePaint); } // 2. Y轴辅助线及文字 Paint axisPaint = Paint() ..color = Colors.grey.withOpacity(0.4) ..strokeWidth = 1.rpx; for (int i = 0; i <= 6; i++) { double y = chartHeight * i / 6; if (i == 6) { // 实线 canvas.drawLine(Offset(xStart, y), Offset(xEnd, y), axisPaint); } else { // 虚线 drawDashedLine( canvas, Offset(xStart, y), Offset(xEnd, y), axisPaint, dashWidth: 8.rpx, dashSpace: 6.rpx, ); } // Y轴文字 TextPainter tp = TextPainter( text: TextSpan( text: '${yMax - (yMax * i / 6).round()}', style: TextStyle( fontSize: 18.rpx, color: themeController.currentColor.sc4), ), textDirection: ui.TextDirection.ltr, ); tp.layout(); tp.paint(canvas, Offset(0, y - tp.height / 2)); } // 3. X轴线 canvas.drawLine( Offset(xStart, chartHeight), Offset(xEnd, chartHeight), axisPaint); // 4. 画X轴时间点对应的垂直虚线辅助线 int totalHours = maxTime.difference(minTime).inHours; int startHour = minTime.hour; for (int i = 1; i < totalHours; i++) { double x = xStart + chartWidth * i / totalHours; // 垂直虚线 drawDashedLine( canvas, Offset(x, 0), Offset(x, chartHeight), axisPaint, dashWidth: 4.rpx, dashSpace: 4.rpx, ); } // 5. 画左侧完整时分 (HH:mm),往内缩 labelInset String leftLabel = DateFormat('HH:mm').format(minTime); TextPainter leftTp = TextPainter( text: TextSpan( text: leftLabel, style: TextStyle( fontSize: 18.rpx, color: themeController.currentColor.sc4, ), ), textDirection: ui.TextDirection.ltr, ); leftTp.layout(); leftTp.paint(canvas, Offset(padding + labelInset - leftTp.width / 2, chartHeight + 8.rpx)); // 6. 画右侧完整时分 (HH:mm),往内缩 labelInset String rightLabel = DateFormat('HH:mm').format(maxTime); TextPainter rightTp = TextPainter( text: TextSpan( text: rightLabel, style: TextStyle( fontSize: 18.rpx, color: themeController.currentColor.sc4, ), ), textDirection: ui.TextDirection.ltr, ); rightTp.layout(); rightTp.paint( canvas, Offset(size.width - padding - labelInset - rightTp.width / 2, chartHeight + 8.rpx)); // 7. 中间小时数字(23, 0, 1, 2, ...) for (int i = 1; i < totalHours; i++) { double x = xStart + chartWidth * i / totalHours; int hourLabelNum = (startHour + i) % 24; String hourLabel = '$hourLabelNum'; TextPainter tp = TextPainter( text: TextSpan( text: hourLabel, style: TextStyle( fontSize: 18.rpx, color: themeController.currentColor.sc4, ), ), textDirection: ui.TextDirection.ltr, ); tp.layout(); tp.paint(canvas, Offset(x - tp.width / 2, chartHeight + 8.rpx)); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; void drawDashedLine( Canvas canvas, Offset start, Offset end, Paint paint, { required double dashWidth, required double dashSpace, }) { final dx = end.dx - start.dx; final dy = end.dy - start.dy; final distance = sqrt(dx * dx + dy * dy); final direction = Offset(dx / distance, dy / distance); double drawn = 0; while (drawn < distance) { final from = start + direction * drawn; final to = start + direction * (drawn + dashWidth).clamp(0, distance); canvas.drawLine(from, to, paint); drawn += dashWidth + dashSpace; } } }