import 'package:ef/ef.dart'; 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 StatefulWidget { // final List> showLabel; // final int startTime; // final int endTime; // final int? threshold; // const LineChartByRange({ // Key? key, // required this.showLabel, // required this.startTime, // required this.endTime, // this.threshold, // 新增 // }) : super(key: key); // @override // State createState() => _LineChartByRangeState(); // } // class _LineChartByRangeState extends State { // Offset? selectedOffset; // Map? selectedData; // @override // Widget build(BuildContext context) { // if (widget.showLabel.isEmpty) return const SizedBox(); // int maxTimes = widget.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(widget.startTime); // DateTime maxTime = DateTime.fromMillisecondsSinceEpoch(widget.endTime); // return GestureDetector( // onTapDown: (details) { // RenderBox box = context.findRenderObject() as RenderBox; // final localPosition = box.globalToLocal(details.globalPosition); // // 查找是否点击到某个点 // for (var item in widget.showLabel) { // int start = item['startTime']; // int end = item['endTime']; // int times = item['times']; // double chartWidth = box.size.width - 40.rpx; // 与 painter 内一致处理 // double chartHeight = box.size.height - 30.rpx; // double xStart = 20.rpx + 12.rpx; // int totalDuration = // maxTime.millisecondsSinceEpoch - minTime.millisecondsSinceEpoch; // double startX = xStart + // chartWidth * // (start - minTime.millisecondsSinceEpoch) / // totalDuration; // double y = chartHeight * (1 - times / yMax); // // 判断点击范围(圆点半径±6.rpx范围) // if ((localPosition - Offset(startX, y)).distance < 10.rpx) { // setState(() { // selectedOffset = Offset(startX, y); // selectedData = item; // }); // return; // } // double endX = xStart + // chartWidth * // (end - minTime.millisecondsSinceEpoch) / // totalDuration; // if ((localPosition - Offset(endX, y)).distance < 10.rpx) { // setState(() { // selectedOffset = Offset(endX, y); // selectedData = item; // }); // return; // } // } // // 没点到,清除选中 // setState(() { // selectedOffset = null; // selectedData = null; // }); // }, // child: Stack( // children: [ // SizedBox( // height: 500.rpx, // child: CustomPaint( // size: Size(double.infinity, 500.rpx), // painter: _LineChartByRangePainter( // data: widget.showLabel, // yMax: yMax, // minTime: minTime, // maxTime: maxTime, // threshold: widget.threshold, // 新增 // ), // ), // ), // if (selectedOffset != null && selectedData != null) // Positioned( // left: selectedOffset!.dx - 60.rpx, // top: selectedOffset!.dy - 50.rpx, // child: Container( // padding: // EdgeInsets.symmetric(horizontal: 12.rpx, vertical: 8.rpx), // decoration: BoxDecoration( // color: Colors.black.withOpacity(0.3), // borderRadius: BorderRadius.circular(10.rpx), // ), // child: Text( // '${DateFormat('HH:mm').format(DateTime.fromMillisecondsSinceEpoch(selectedData!['startTime']))} - ' // '${DateFormat('HH:mm').format(DateTime.fromMillisecondsSinceEpoch(selectedData!['endTime']))}\n' // '次数: ${selectedData!['times']}', // style: TextStyle( // fontSize: 18.rpx, // color: Colors.white, // ), // ), // ), // ), // ], // ), // ); // } // } // class _LineChartByRangePainter extends CustomPainter { // final List> data; // final int yMax; // final DateTime minTime; // final DateTime maxTime; // final int? threshold; // _LineChartByRangePainter({ // required this.data, // required this.yMax, // required this.minTime, // required this.maxTime, // this.threshold, // }); // @override // void paint(Canvas canvas, Size size) { // double padding = 20.rpx; // double labelInset = 12.rpx; // 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 axisPaint = Paint() // ..color = Colors.grey.withOpacity(0.4) // ..strokeWidth = 1.rpx; // Paint thresholdPaint = Paint() // ..color = themeController.currentColor.sc9 // ..strokeWidth = 1.rpx; // // 1. 阈值虚线(红色) // if (threshold != null && threshold! >= 0 && threshold! <= yMax) { // double yThreshold = chartHeight * (1 - threshold! / yMax); // drawDashedLine( // canvas, // Offset(xStart, yThreshold), // Offset(xEnd, yThreshold), // thresholdPaint, // dashWidth: 8.rpx, // dashSpace: 6.rpx, // ); // } // // 2. 绘制数据线段和圆点 // 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); // // 设置颜色(根据 threshold 判断) // Color pointColor; // if (threshold != null && times >= threshold!) { // pointColor = themeController.currentColor.sc9; // } else { // pointColor = stringToColor("#00C1AA"); // } // Paint dynamicLinePaint = Paint() // ..style = PaintingStyle.stroke // ..strokeWidth = 3.rpx // ..color = pointColor // ..strokeCap = StrokeCap.round; // Paint dynamicCirclePaint = Paint() // ..style = PaintingStyle.fill // ..color = pointColor; // // 画线段 // canvas.drawLine(Offset(startX, y), Offset(endX, y), dynamicLinePaint); // // 画起点和终点圆点 // canvas.drawCircle(Offset(startX, y), 6.rpx, dynamicCirclePaint); // canvas.drawCircle(Offset(endX, y), 6.rpx, dynamicCirclePaint); // } // // 3. Y轴辅助线和文字 // 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, // ); // } // 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)); // } // // 4. X轴主线 // canvas.drawLine( // Offset(xStart, chartHeight), // Offset(xEnd, chartHeight), // axisPaint, // ); // // 5. X轴时间文字(左右两侧) // 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)); // 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)); // // 6. 中间小时刻度 // int totalHours = maxTime.difference(minTime).inHours + 1; // int startHour = minTime.hour; // 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; // } // } // } 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 StatefulWidget { final List> showLabel; final int startTime; final int endTime; final int? threshold; /// 新增外部指定的 Y 轴最大值 final int maxY; /// Y 轴分段数,默认6段 final int ySegments; const LineChartByRange({ Key? key, required this.showLabel, required this.startTime, required this.endTime, required this.maxY, this.threshold, this.ySegments = 6, }) : super(key: key); @override State createState() => _LineChartByRangeState(); } class _LineChartByRangeState extends State { Offset? selectedOffset; Map? selectedData; @override Widget build(BuildContext context) { if (widget.showLabel.isEmpty) return const SizedBox(); DateTime minTime = DateTime.fromMillisecondsSinceEpoch(widget.startTime); DateTime maxTime = DateTime.fromMillisecondsSinceEpoch(widget.endTime); return GestureDetector( onTapDown: (details) { RenderBox box = context.findRenderObject() as RenderBox; final localPosition = box.globalToLocal(details.globalPosition); // 查找是否点击到某个点 for (var item in widget.showLabel) { int start = item['startTime']; int end = item['endTime']; int times = item['times']; double chartWidth = box.size.width - 40.rpx; // 与 painter 内一致处理 double chartHeight = box.size.height - 30.rpx; double xStart = 20.rpx + 12.rpx; int totalDuration = maxTime.millisecondsSinceEpoch - minTime.millisecondsSinceEpoch; double startX = xStart + chartWidth * (start - minTime.millisecondsSinceEpoch) / totalDuration; double y = chartHeight * (1 - times / widget.maxY); // 判断点击范围(圆点半径±6.rpx范围) if ((localPosition - Offset(startX, y)).distance < 10.rpx) { setState(() { selectedOffset = Offset(startX, y); selectedData = item; }); return; } double endX = xStart + chartWidth * (end - minTime.millisecondsSinceEpoch) / totalDuration; if ((localPosition - Offset(endX, y)).distance < 10.rpx) { setState(() { selectedOffset = Offset(endX, y); selectedData = item; }); return; } } // 没点到,清除选中 setState(() { selectedOffset = null; selectedData = null; }); }, child: Stack( children: [ SizedBox( height: 500.rpx, child: CustomPaint( size: Size(double.infinity, 500.rpx), painter: _LineChartByRangePainter( data: widget.showLabel, maxY: widget.maxY, minTime: minTime, maxTime: maxTime, threshold: widget.threshold, ySegments: widget.ySegments, ), ), ), if (selectedOffset != null && selectedData != null) Positioned( left: selectedOffset!.dx - 60.rpx, top: selectedOffset!.dy - 50.rpx, child: Container( padding: EdgeInsets.symmetric(horizontal: 12.rpx, vertical: 8.rpx), decoration: BoxDecoration( color: Colors.black.withOpacity(0.3), borderRadius: BorderRadius.circular(10.rpx), ), child: Text( '${DateFormat('HH:mm').format(DateTime.fromMillisecondsSinceEpoch(selectedData!['startTime']))} - ' '${DateFormat('HH:mm').format(DateTime.fromMillisecondsSinceEpoch(selectedData!['endTime']))}\n' "时长".tr+ ' ${selectedData!['times']}', style: TextStyle( fontSize: 18.rpx, color: Colors.white, ), ), ), ), ], ), ); } } class _LineChartByRangePainter extends CustomPainter { final List> data; final int maxY; final DateTime minTime; final DateTime maxTime; final int? threshold; final int ySegments; _LineChartByRangePainter({ required this.data, required this.maxY, required this.minTime, required this.maxTime, this.threshold, this.ySegments = 6, }); @override void paint(Canvas canvas, Size size) { double padding = 20.rpx; double labelInset = 12.rpx; 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 axisPaint = Paint() ..color = Colors.grey.withOpacity(0.4) ..strokeWidth = 1.rpx; Paint thresholdPaint = Paint() ..color = themeController.currentColor.sc9 ..strokeWidth = 1.rpx; // 阈值虚线(红色) if (threshold != null && threshold! >= 0 && threshold! <= maxY) { double yThreshold = chartHeight * (1 - threshold! / maxY); drawDashedLine( canvas, Offset(xStart, yThreshold), Offset(xEnd, yThreshold), thresholdPaint, dashWidth: 8.rpx, dashSpace: 6.rpx, ); } // 绘制数据线段和圆点 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 / maxY); // 设置颜色(根据 threshold 判断) Color pointColor; if (threshold != null && times >= threshold!) { pointColor = themeController.currentColor.sc9; } else { pointColor = stringToColor("#00C1AA"); } Paint dynamicLinePaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 3.rpx ..color = pointColor ..strokeCap = StrokeCap.round; Paint dynamicCirclePaint = Paint() ..style = PaintingStyle.fill ..color = pointColor; // 画线段 canvas.drawLine(Offset(startX, y), Offset(endX, y), dynamicLinePaint); // 画起点和终点圆点 canvas.drawCircle(Offset(startX, y), 6.rpx, dynamicCirclePaint); canvas.drawCircle(Offset(endX, y), 6.rpx, dynamicCirclePaint); } // Y轴辅助线和文字 for (int i = 0; i <= ySegments; i++) { double y = chartHeight * i / ySegments; if (i == ySegments) { 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, ); } TextPainter tp = TextPainter( text: TextSpan( text: '${maxY - (maxY * i / ySegments).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)); } // X轴主线 canvas.drawLine( Offset(xStart, chartHeight), Offset(xEnd, chartHeight), axisPaint, ); // X轴时间文字(左右两侧) 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)); 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)); // 中间小时刻度 int totalHours = maxTime.difference(minTime).inHours + 1; int startHour = minTime.hour; 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; } } }