765 lines
23 KiB
Dart
765 lines
23 KiB
Dart
// 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<BarData> 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<BarChartWidget> createState() => _BarChartWidgetState();
|
||
// }
|
||
|
||
// class _BarChartWidgetState extends State<BarChartWidget> {
|
||
// 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<BarData> 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<BarData> 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<BarChartWidget> createState() => _BarChartWidgetState();
|
||
}
|
||
|
||
class _BarChartWidgetState extends State<BarChartWidget> {
|
||
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<BarData> 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;
|
||
}
|
||
}
|
||
}
|