// import 'dart:ui' as ui; // //柱形图显示 // import 'package:flutter/material.dart'; // import 'package:intl/intl.dart'; // import 'package:vbvs_app/common/color/appConstants.dart'; // import 'package:vbvs_app/common/util/FitTool.dart'; // import 'package:vbvs_app/common/util/MyUtils.dart'; // class BarData { // final int st; // 起始时间(毫秒) // final int et; // 结束时间(毫秒) // final double value; // 柱子高度 // final int id; // final String name; // final Color color; // BarData({ // required this.st, // required this.et, // required this.value, // required this.id, // required this.name, // required this.color, // }); // } // class BarChartWidget extends StatefulWidget { // final List data; // final int startTime; // 毫秒时间戳 // final int endTime; // 毫秒时间戳 // final double maxYValue; // Y轴最大值 // final int yStepCount; // Y轴分段数 // const BarChartWidget({ // super.key, // required this.data, // required this.startTime, // required this.endTime, // required this.maxYValue, // this.yStepCount = 5, // }); // @override // State createState() => _BarChartWidgetState(); // } // class _BarChartWidgetState extends State { // BarData? selectedBar; // void _handleTapOrDrag(Offset localPosition, Size size) { // final chartWidth = size.width - 30.rpx; // final totalDuration = widget.endTime - widget.startTime; // for (final d in widget.data) { // final left = // ((d.st - widget.startTime) / totalDuration) * chartWidth + 30.rpx; // final right = // ((d.et - widget.startTime) / totalDuration) * chartWidth + 30.rpx; // if (localPosition.dx >= left && localPosition.dx <= right) { // setState(() { // selectedBar = d; // }); // return; // } // } // setState(() { // selectedBar = null; // }); // } // @override // Widget build(BuildContext context) { // return LayoutBuilder(builder: (context, constraints) { // return GestureDetector( // behavior: HitTestBehavior.opaque, // onPanDown: (details) => // _handleTapOrDrag(details.localPosition, constraints.biggest), // onPanUpdate: (details) => // _handleTapOrDrag(details.localPosition, constraints.biggest), // onTapDown: (details) => // _handleTapOrDrag(details.localPosition, constraints.biggest), // child: CustomPaint( // size: Size(constraints.maxWidth, 250.rpx), // 使用约束的最大宽度 // painter: BarChartPainter( // widget.data, // widget.startTime, // widget.endTime, // maxYValue: widget.maxYValue, // yStepCount: widget.yStepCount, // selectedBar: selectedBar, // ), // ), // ); // }); // } // } // class BarChartPainter extends CustomPainter { // final List data; // final int startTime; // final int endTime; // final double maxYValue; // final int yStepCount; // final BarData? selectedBar; // final double topPadding = 0; // final double bottomPadding = 0; // final double leftPadding = 30.rpx; // BarChartPainter( // this.data, // this.startTime, // this.endTime, { // required this.maxYValue, // this.yStepCount = 5, // this.selectedBar, // }); // @override // void paint(Canvas canvas, Size size) { // final chartWidth = size.width - leftPadding; // final chartHeight = size.height - topPadding - bottomPadding; // final totalDuration = endTime - startTime; // final textPainter = TextPainter(textDirection: ui.TextDirection.ltr); // final stepValue = maxYValue / yStepCount; // // Y轴刻度 // // for (int i = 0; i <= yStepCount; i++) { // // final value = stepValue * i; // // final y = topPadding + chartHeight - (value / maxYValue) * chartHeight; // // final dashPaint = Paint() // // ..color = Colors.grey.withOpacity(0.4) // // ..strokeWidth = 1.rpx; // // drawDashedLine( // // canvas, Offset(leftPadding, y), Offset(size.width, y), dashPaint); // // textPainter.text = TextSpan( // // text: value.toStringAsFixed(0), // // style: TextStyle( // // fontSize: 18.rpx, // // color: themeController.currentColor.sc4, // // ), // // ); // // textPainter.layout(); // // textPainter.paint( // // canvas, // // Offset(leftPadding - textPainter.width - 4, y - textPainter.height / 2), // // ); // // } // // Y轴刻度 // for (int i = 0; i <= yStepCount; i++) { // final value = stepValue * i; // final y = topPadding + chartHeight - (value / maxYValue) * chartHeight; // // 判断是否是基线(i == 0) // final bool isBaseline = i == 0; // if (isBaseline) { // // 基线画实线 // final baselinePaint = Paint() // ..color = Colors.grey.withOpacity(0.6) // ..strokeWidth = 1.rpx // ..style = PaintingStyle.stroke; // canvas.drawLine( // Offset(leftPadding, y), // Offset(size.width, y), // baselinePaint, // ); // } else { // // 其他刻度画虚线 // final dashPaint = Paint() // ..color = Colors.grey.withOpacity(0.4) // ..strokeWidth = 1.rpx; // drawDashedLine( // canvas, // Offset(leftPadding, y), // Offset(size.width, y), // dashPaint, // ); // } // // 绘制刻度文字 // textPainter.text = TextSpan( // text: value.toStringAsFixed(0), // style: TextStyle( // fontSize: 18.rpx, // color: themeController.currentColor.sc4, // ), // ); // textPainter.layout(); // textPainter.paint( // canvas, // Offset(leftPadding - textPainter.width - 4, y - textPainter.height / 2), // ); // } // // X轴刻度 - 参考横线图的24小时制 // final startDate = DateTime.fromMillisecondsSinceEpoch(startTime); // final endDate = DateTime.fromMillisecondsSinceEpoch(endTime); // final xAxisY = topPadding + chartHeight; // 这是最底部的Y坐标 // // 计算总小时数 // final totalHours = endDate.difference(startDate).inHours + 1; // final startHour = startDate.hour; // // 绘制X轴主线(实线) // // final xAxisPaint = Paint() // // ..color = Colors.grey.withOpacity(0.4) // // ..strokeWidth = 1.rpx; // // canvas.drawLine( // // Offset(leftPadding, xAxisY), // // Offset(size.width, xAxisY), // // xAxisPaint, // // ); // // 绘制左右两侧时间标签(HH:mm格式) // final leftLabel = DateFormat('HH:mm').format(startDate); // textPainter.text = TextSpan( // text: leftLabel, // style: TextStyle( // fontSize: 18.rpx, // color: themeController.currentColor.sc4, // ), // ); // textPainter.layout(); // textPainter.paint( // canvas, Offset(leftPadding - textPainter.width / 2, xAxisY + 8.rpx)); // final rightLabel = DateFormat('HH:mm').format(endDate); // textPainter.text = TextSpan( // text: rightLabel, // style: TextStyle( // fontSize: 18.rpx, // color: themeController.currentColor.sc4, // ), // ); // textPainter.layout(); // textPainter.paint( // canvas, Offset(size.width - textPainter.width / 2, xAxisY + 8.rpx)); // // 绘制中间小时刻度(只显示小时数字) // for (int i = 1; i < totalHours; i++) { // final double x = leftPadding + chartWidth * i / totalHours; // int hourLabelNum = (startHour + i) % 24; // final hourLabel = '$hourLabelNum'; // textPainter.text = TextSpan( // text: hourLabel, // style: TextStyle( // fontSize: 18.rpx, // color: themeController.currentColor.sc4, // ), // ); // textPainter.layout(); // textPainter.paint( // canvas, Offset(x - textPainter.width / 2, xAxisY + 8.rpx)); // } // // 柱子绘制 & 提示信息缓存 // Offset? tipOffset; // Size? tipSize; // String? tipText; // Color tipColor = Colors.black; // for (final d in data) { // final left = // ((d.st - startTime) / totalDuration) * chartWidth + leftPadding; // final right = // ((d.et - startTime) / totalDuration) * chartWidth + leftPadding; // final barHeight = (d.value / maxYValue) * chartHeight; // final top = topPadding + chartHeight - barHeight; // final barPaint = Paint()..color = d.color; // canvas.drawRect( // Rect.fromLTRB(left, top, right, topPadding + chartHeight), // barPaint, // ); // // 缓存 tip 信息 // if (selectedBar == d) { // tipText = // '${d.name}\n${d.value.toStringAsFixed(1)}次\n${MyUtils.formatToHHmm(d.st)}'; // final tp = TextPainter( // text: TextSpan( // text: tipText, // style: TextStyle(fontSize: 16.rpx, color: Colors.white), // ), // textAlign: TextAlign.center, // textDirection: ui.TextDirection.ltr, // ); // tp.layout(); // final tipWidth = tp.width + 20.rpx; // final tipHeight = tp.height + 10.rpx; // final tipLeft = left + (right - left) / 2 - tipWidth / 2; // final tipTop = top - tipHeight - 10.rpx; // // 确保tip不会超出画布顶部 // final double adjustedTipTop = tipTop < 0 ? 0.0 : tipTop; // tipOffset = Offset(tipLeft, adjustedTipTop); // tipSize = Size(tipWidth, tipHeight); // tipColor = Colors.black.withOpacity(0.8); // } // } // // 绘制 tip(在柱子之上) // if (tipText != null && tipOffset != null && tipSize != null) { // final rect = RRect.fromRectAndRadius( // Rect.fromLTWH( // tipOffset.dx, tipOffset.dy, tipSize.width, tipSize.height), // Radius.circular(8.rpx), // ); // final tipBgPaint = Paint()..color = tipColor; // canvas.drawRRect(rect, tipBgPaint); // final tipTextPainter = TextPainter( // text: TextSpan( // text: tipText, // style: TextStyle(fontSize: 16.rpx, color: Colors.white), // ), // textAlign: TextAlign.center, // textDirection: ui.TextDirection.ltr, // ); // tipTextPainter.layout(); // tipTextPainter.paint( // canvas, // Offset(tipOffset.dx + 10.rpx, tipOffset.dy + 5.rpx), // ); // } // } // @override // bool shouldRepaint(covariant CustomPainter oldDelegate) => true; // void drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint, // {double dashWidth = 5, double dashSpace = 3}) { // double totalLength = (end.dx - start.dx).abs(); // double dashCount = (totalLength / (dashWidth + dashSpace)).floorToDouble(); // double dx = start.dx; // final dy = start.dy; // for (int i = 0; i < dashCount; i++) { // final from = Offset(dx, dy); // final to = Offset(dx + dashWidth, dy); // canvas.drawLine(from, to, paint); // dx += dashWidth + dashSpace; // } // } // } import 'dart:ui' as ui; //柱形图显示 import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:vbvs_app/common/color/appConstants.dart'; import 'package:vbvs_app/common/util/FitTool.dart'; import 'package:vbvs_app/common/util/MyUtils.dart'; class BarData { final int st; // 起始时间(毫秒) final int et; // 结束时间(毫秒) final double value; // 柱子高度 final int id; final String name; final Color color; BarData({ required this.st, required this.et, required this.value, required this.id, required this.name, required this.color, }); } class BarChartWidget extends StatefulWidget { final List data; final int startTime; // 毫秒时间戳 final int endTime; // 毫秒时间戳 final double maxYValue; // Y轴最大值 final int yStepCount; // Y轴分段数 const BarChartWidget({ super.key, required this.data, required this.startTime, required this.endTime, required this.maxYValue, this.yStepCount = 5, }); @override State createState() => _BarChartWidgetState(); } class _BarChartWidgetState extends State { BarData? selectedBar; void _handleTapOrDrag(Offset localPosition, Size size) { final chartWidth = size.width - 30.rpx; final totalDuration = widget.endTime - widget.startTime; for (final d in widget.data) { final left = ((d.st - widget.startTime) / totalDuration) * chartWidth + 30.rpx; final right = ((d.et - widget.startTime) / totalDuration) * chartWidth + 30.rpx; if (localPosition.dx >= left && localPosition.dx <= right) { setState(() { selectedBar = d; }); return; } } setState(() { selectedBar = null; }); } @override Widget build(BuildContext context) { return LayoutBuilder(builder: (context, constraints) { return GestureDetector( behavior: HitTestBehavior.opaque, onPanDown: (details) => _handleTapOrDrag(details.localPosition, constraints.biggest), onPanUpdate: (details) => _handleTapOrDrag(details.localPosition, constraints.biggest), onTapDown: (details) => _handleTapOrDrag(details.localPosition, constraints.biggest), child: CustomPaint( size: Size(constraints.maxWidth, 250.rpx), // 使用约束的最大宽度 painter: BarChartPainter( widget.data, widget.startTime, widget.endTime, maxYValue: widget.maxYValue, yStepCount: widget.yStepCount, selectedBar: selectedBar, ), ), ); }); } } class BarChartPainter extends CustomPainter { final List data; final int startTime; final int endTime; final double maxYValue; final int yStepCount; final BarData? selectedBar; final double topPadding = 0; final double bottomPadding = 0; final double leftPadding = 30.rpx; BarChartPainter( this.data, this.startTime, this.endTime, { required this.maxYValue, this.yStepCount = 5, this.selectedBar, }); @override void paint(Canvas canvas, Size size) { final chartWidth = size.width - leftPadding; final chartHeight = size.height - topPadding - bottomPadding; final totalDuration = endTime - startTime; final textPainter = TextPainter(textDirection: ui.TextDirection.ltr); final stepValue = maxYValue / yStepCount; // Y轴刻度 for (int i = 0; i <= yStepCount; i++) { final value = stepValue * i; final y = topPadding + chartHeight - (value / maxYValue) * chartHeight; // 判断是否是基线(i == 0) final bool isBaseline = i == 0; if (isBaseline) { // 基线画实线 final baselinePaint = Paint() ..color = Colors.grey.withOpacity(0.6) ..strokeWidth = 1.rpx ..style = PaintingStyle.stroke; canvas.drawLine( Offset(leftPadding, y), Offset(size.width, y), baselinePaint, ); } else { // 其他刻度画虚线 final dashPaint = Paint() ..color = Colors.grey.withOpacity(0.4) ..strokeWidth = 1.rpx; drawDashedLine( canvas, Offset(leftPadding, y), Offset(size.width, y), dashPaint, ); } // 绘制刻度文字 textPainter.text = TextSpan( text: value.toStringAsFixed(0), style: TextStyle( fontSize: 18.rpx, color: themeController.currentColor.sc4, ), ); textPainter.layout(); textPainter.paint( canvas, Offset(leftPadding - textPainter.width - 4, y - textPainter.height / 2), ); } // X轴刻度 - 基于实际时间而不是均匀分布 final startDate = DateTime.fromMillisecondsSinceEpoch(startTime); final endDate = DateTime.fromMillisecondsSinceEpoch(endTime); final xAxisY = topPadding + chartHeight; // 这是最底部的Y坐标 // 计算总小时数 final int hourMs = 60 * 60 * 1000; final int totalHours = (endTime - startTime) ~/ hourMs; // 绘制左右两侧时间标签(HH:mm格式) final leftLabel = DateFormat('HH:mm').format(startDate); textPainter.text = TextSpan( text: leftLabel, style: TextStyle( fontSize: 18.rpx, color: themeController.currentColor.sc4, ), ); textPainter.layout(); textPainter.paint( canvas, Offset(leftPadding - textPainter.width / 2, xAxisY + 8.rpx)); final rightLabel = DateFormat('HH:mm').format(endDate); textPainter.text = TextSpan( text: rightLabel, style: TextStyle( fontSize: 18.rpx, color: themeController.currentColor.sc4, ), ); textPainter.layout(); textPainter.paint( canvas, Offset(size.width - textPainter.width / 2, xAxisY + 8.rpx)); // 绘制中间小时刻度 - 参考SnoreChartContainer的逻辑 if (totalHours <= 8) { // 显示每个整点 DateTime currentHour = DateTime( startDate.year, startDate.month, startDate.day, startDate.hour, ); // 如果起始时间不是整点,从下一个整点开始 if (startDate.minute > 0 || startDate.second > 0 || startDate.millisecond > 0) { currentHour = currentHour.add(Duration(hours: 1)); } while (currentHour.millisecondsSinceEpoch < endTime) { final int timeMs = currentHour.millisecondsSinceEpoch; if (timeMs > startTime && timeMs < endTime) { // 检查是否太靠近边界 if (timeMs - startTime < 10 * 60 * 1000 || endTime - timeMs < 10 * 60 * 1000) { currentHour = currentHour.add(Duration(hours: 1)); continue; } // 计算从起始时间到当前整点的分钟数 final double x = leftPadding + ((timeMs - startTime) / totalDuration) * chartWidth; final hourLabel = '${currentHour.hour}'; textPainter.text = TextSpan( text: hourLabel, style: TextStyle( fontSize: 18.rpx, color: themeController.currentColor.sc4, ), ); textPainter.layout(); textPainter.paint( canvas, Offset(x - textPainter.width / 2, xAxisY + 8.rpx), ); } currentHour = currentHour.add(Duration(hours: 1)); } } else { // 超过8小时,跳着显示 final int labelInterval = (totalHours / 6).ceil(); // 分成大约6个标签 DateTime firstLabelHour = DateTime( startDate.year, startDate.month, startDate.day, startDate.hour + (labelInterval - (startDate.hour % labelInterval)), 0, 0, 0, 0, ); // 确保第一个标签在开始时间之后 if (firstLabelHour.millisecondsSinceEpoch <= startTime) { firstLabelHour = firstLabelHour.add(Duration(hours: labelInterval)); } DateTime currentHour = firstLabelHour; while (currentHour.millisecondsSinceEpoch < endTime) { final int timeMs = currentHour.millisecondsSinceEpoch; // 确保标签离边界足够远 if (timeMs - startTime >= hourMs && endTime - timeMs >= hourMs) { final double x = leftPadding + ((timeMs - startTime) / totalDuration) * chartWidth; final hourLabel = '${currentHour.hour}'; textPainter.text = TextSpan( text: hourLabel, style: TextStyle( fontSize: 18.rpx, color: themeController.currentColor.sc4, ), ); textPainter.layout(); textPainter.paint( canvas, Offset(x - textPainter.width / 2, xAxisY + 8.rpx), ); } currentHour = currentHour.add(Duration(hours: labelInterval)); } } // 柱子绘制 & 提示信息缓存 Offset? tipOffset; Size? tipSize; String? tipText; Color tipColor = Colors.black; for (final d in data) { final left = ((d.st - startTime) / totalDuration) * chartWidth + leftPadding; final right = ((d.et - startTime) / totalDuration) * chartWidth + leftPadding; final barHeight = (d.value / maxYValue) * chartHeight; final top = topPadding + chartHeight - barHeight; final barPaint = Paint()..color = d.color; canvas.drawRect( Rect.fromLTRB(left, top, right, topPadding + chartHeight), barPaint, ); // 缓存 tip 信息 if (selectedBar == d) { tipText = '${d.name}\n${d.value.toStringAsFixed(1)}次\n${MyUtils.formatToHHmm(d.st)}'; final tp = TextPainter( text: TextSpan( text: tipText, style: TextStyle(fontSize: 16.rpx, color: Colors.white), ), textAlign: TextAlign.center, textDirection: ui.TextDirection.ltr, ); tp.layout(); final tipWidth = tp.width + 20.rpx; final tipHeight = tp.height + 10.rpx; final tipLeft = left + (right - left) / 2 - tipWidth / 2; final tipTop = top - tipHeight - 10.rpx; // 确保tip不会超出画布顶部 final double adjustedTipTop = tipTop < 0 ? 0.0 : tipTop; tipOffset = Offset(tipLeft, adjustedTipTop); tipSize = Size(tipWidth, tipHeight); tipColor = Colors.black.withOpacity(0.8); } } // 绘制 tip(在柱子之上) if (tipText != null && tipOffset != null && tipSize != null) { final rect = RRect.fromRectAndRadius( Rect.fromLTWH( tipOffset.dx, tipOffset.dy, tipSize.width, tipSize.height), Radius.circular(8.rpx), ); final tipBgPaint = Paint()..color = tipColor; canvas.drawRRect(rect, tipBgPaint); final tipTextPainter = TextPainter( text: TextSpan( text: tipText, style: TextStyle(fontSize: 16.rpx, color: Colors.white), ), textAlign: TextAlign.center, textDirection: ui.TextDirection.ltr, ); tipTextPainter.layout(); tipTextPainter.paint( canvas, Offset(tipOffset.dx + 10.rpx, tipOffset.dy + 5.rpx), ); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; void drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint, {double dashWidth = 5, double dashSpace = 3}) { double totalLength = (end.dx - start.dx).abs(); double dashCount = (totalLength / (dashWidth + dashSpace)).floorToDouble(); double dx = start.dx; final dy = start.dy; for (int i = 0; i < dashCount; i++) { final from = Offset(dx, dy); final to = Offset(dx + dashWidth, dy); canvas.drawLine(from, to, paint); dx += dashWidth + dashSpace; } } }