修复日报中图标时间轴错乱。

This commit is contained in:
wyf
2026-01-08 11:56:17 +08:00
parent 575f91e8dd
commit 2991deb8b3
21 changed files with 2824 additions and 1055 deletions

View File

@@ -1,3 +1,361 @@
// import 'dart:math';
// import 'dart:ui' as ui;
// 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';
// class LineChartByRange extends StatefulWidget {
// final List<Map<String, dynamic>> 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<LineChartByRange> createState() => _LineChartByRangeState();
// }
// class _LineChartByRangeState extends State<LineChartByRange> {
// Offset? selectedOffset;
// Map<String, dynamic>? 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<Map<String, dynamic>> 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'];
// int times = item['times'];
// double startX = xStart * 2 +
// chartWidth * (start - minTime.millisecondsSinceEpoch) / totalDuration;
// double endX = xStart * 2 +
// 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;
// }
// }
// }
import 'dart:math';
import 'dart:ui' as ui;
@@ -199,7 +557,6 @@ class _LineChartByRangePainter extends CustomPainter {
for (var item in data) {
int start = item['startTime'];
int end = item['endTime'];
// int times = item['times'];
int times = item['times'];
double startX = xStart * 2 +
@@ -305,28 +662,121 @@ class _LineChartByRangePainter extends CustomPainter {
Offset(size.width - padding - labelInset - rightTp.width / 2,
chartHeight + 8.rpx));
// 中间小时刻度
int totalHours = maxTime.difference(minTime).inHours + 1;
int startHour = minTime.hour;
// 中间小时刻度 - 使用实际时间比例
final int hourMs = 60 * 60 * 1000;
final int totalHours =
(maxTime.millisecondsSinceEpoch - minTime.millisecondsSinceEpoch) ~/
hourMs;
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,
if (totalHours <= 8) {
// 显示每个整点
DateTime currentHour = DateTime(
minTime.year,
minTime.month,
minTime.day,
minTime.hour,
);
tp.layout();
tp.paint(canvas, Offset(x - tp.width / 2, chartHeight + 8.rpx));
// 如果起始时间不是整点,从下一个整点开始
if (minTime.minute > 0 || minTime.second > 0 || minTime.millisecond > 0) {
currentHour = currentHour.add(Duration(hours: 1));
}
while (
currentHour.millisecondsSinceEpoch < maxTime.millisecondsSinceEpoch) {
final int timeMs = currentHour.millisecondsSinceEpoch;
if (timeMs > minTime.millisecondsSinceEpoch &&
timeMs < maxTime.millisecondsSinceEpoch) {
// 检查是否太靠近边界
if (timeMs - minTime.millisecondsSinceEpoch < 10 * 60 * 1000 ||
maxTime.millisecondsSinceEpoch - timeMs < 10 * 60 * 1000) {
currentHour = currentHour.add(Duration(hours: 1));
continue;
}
// 计算从起始时间到当前整点的位置
final double x = xStart +
chartWidth *
(timeMs - minTime.millisecondsSinceEpoch) /
totalDuration;
final hourLabel = '${currentHour.hour}';
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),
);
}
currentHour = currentHour.add(Duration(hours: 1));
}
} else {
// 超过8小时跳着显示
final int labelInterval = (totalHours / 6).ceil(); // 分成大约6个标签
DateTime firstLabelHour = DateTime(
minTime.year,
minTime.month,
minTime.day,
minTime.hour + (labelInterval - (minTime.hour % labelInterval)),
0,
0,
0,
0,
);
// 确保第一个标签在开始时间之后
if (firstLabelHour.millisecondsSinceEpoch <=
minTime.millisecondsSinceEpoch) {
firstLabelHour = firstLabelHour.add(Duration(hours: labelInterval));
}
DateTime currentHour = firstLabelHour;
while (
currentHour.millisecondsSinceEpoch < maxTime.millisecondsSinceEpoch) {
final int timeMs = currentHour.millisecondsSinceEpoch;
// 确保标签离边界足够远
if (timeMs - minTime.millisecondsSinceEpoch >= hourMs &&
maxTime.millisecondsSinceEpoch - timeMs >= hourMs) {
final double x = xStart +
chartWidth *
(timeMs - minTime.millisecondsSinceEpoch) /
totalDuration;
final hourLabel = '${currentHour.hour}';
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),
);
}
currentHour = currentHour.add(Duration(hours: labelInterval));
}
}
}

View File

@@ -1,3 +1,364 @@
// 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;
//柱形图显示
@@ -127,31 +488,6 @@ class BarChartPainter extends CustomPainter {
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;
@@ -201,24 +537,14 @@ class BarChartPainter extends CustomPainter {
);
}
// X轴刻度 - 参考横线图的24小时制
// X轴刻度 - 基于实际时间而不是均匀分布
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,
// );
final int hourMs = 60 * 60 * 1000;
final int totalHours = (endTime - startTime) ~/ hourMs;
// 绘制左右两侧时间标签HH:mm格式
final leftLabel = DateFormat('HH:mm').format(startDate);
@@ -245,23 +571,101 @@ class BarChartPainter extends CustomPainter {
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,
),
// 绘制中间小时刻度 - 参考SnoreChartContainer的逻辑
if (totalHours <= 8) {
// 显示每个整点
DateTime currentHour = DateTime(
startDate.year,
startDate.month,
startDate.day,
startDate.hour,
);
textPainter.layout();
textPainter.paint(
canvas, Offset(x - textPainter.width / 2, xAxisY + 8.rpx));
// 如果起始时间不是整点,从下一个整点开始
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));
}
}
// 柱子绘制 & 提示信息缓存
@@ -358,330 +762,3 @@ class BarChartPainter extends CustomPainter {
}
}
}
//横线显示
// import 'dart:math';
// import 'dart:ui' as ui;
// 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';
// 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) {
// print('点击位置: $localPosition, 画布大小: $size');
// final chartWidth = size.width - 30.rpx;
// final totalDuration = widget.endTime - widget.startTime;
// // 使用与绘制相同的chartHeight计算方式
// final double topPadding = 0;
// final double bottomPadding = 0;
// final double leftPadding = 30.rpx;
// final double chartHeight = size.height - topPadding - bottomPadding;
// for (final d in widget.data) {
// final left = ((d.st - widget.startTime) / totalDuration) * chartWidth +
// leftPadding;
// final right = ((d.et - widget.startTime) / totalDuration) * chartWidth +
// leftPadding;
// // 使用与绘制相同的Y坐标计算方式
// final y = topPadding + chartHeight * (1 - d.value / widget.maxYValue);
// // 判断点击是否在横线附近(增加容差范围)
// if (localPosition.dx >= left - 5.rpx &&
// localPosition.dx <= right + 5.rpx &&
// (localPosition.dy - y).abs() < 15.rpx) {
// 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(double.infinity, 500.rpx),
// size: Size(constraints.maxWidth, 500.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 = maxYValue - (stepValue * i); // 从最大值开始递减
// final y = topPadding + chartHeight * i / yStepCount;
// final dashPaint = Paint()
// ..color = Colors.grey.withOpacity(0.4)
// ..strokeWidth = 1.rpx;
// // 最上面的线i == ySegments是实线
// if (i == yStepCount) {
// canvas.drawLine(
// Offset(leftPadding, y), Offset(size.width, y), dashPaint);
// } else {
// // 其他线都是虚线
// 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,
// );
// // 绘制左右两侧时间标签
// 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));
// }
// // 绘制数据横线(根据数据值绘制水平线段)
// for (final d in data) {
// final left =
// ((d.st - startTime) / totalDuration) * chartWidth + leftPadding;
// final right =
// ((d.et - startTime) / totalDuration) * chartWidth + leftPadding;
// // 根据value计算Y位置0在底部maxYValue在顶部
// final y = topPadding + chartHeight * (1 - d.value / maxYValue);
// final linePaint = Paint()
// ..style = PaintingStyle.stroke
// ..strokeWidth = 3.rpx
// ..color = d.color
// ..strokeCap = StrokeCap.round;
// // 画水平线段
// canvas.drawLine(Offset(left, y), Offset(right, y), linePaint);
// }
// // 如果选中了某条横线,显示提示信息
// // 如果选中了某条横线,显示提示信息
// if (selectedBar != null) {
// final d = selectedBar!;
// final left =
// ((d.st - startTime) / totalDuration) * chartWidth + leftPadding;
// final right =
// ((d.et - startTime) / totalDuration) * chartWidth + leftPadding;
// final y = topPadding + chartHeight * (1 - d.value / maxYValue);
// final 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 = y - tipHeight - 10.rpx;
// // 确保tip不会超出画布顶部 - 明确转换为double
// final double adjustedTipTop = tipTop < 0 ? 0.0 : tipTop;
// // 绘制tip背景
// final rect = RRect.fromRectAndRadius(
// Rect.fromLTWH(tipLeft, adjustedTipTop, tipWidth, tipHeight),
// Radius.circular(8.rpx),
// );
// final tipBgPaint = Paint()..color = Colors.black.withOpacity(0.8);
// canvas.drawRRect(rect, tipBgPaint);
// // 绘制tip文字
// tp.paint(
// canvas,
// Offset(tipLeft + 10.rpx, adjustedTipTop + 5.rpx),
// );
// }
// }
// @override
// bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
// void drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint,
// {double dashWidth = 4, double dashSpace = 3}) {
// 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;
// }
// }
// }

View File

@@ -1,410 +1,3 @@
// import 'dart:ui' as ui;
// import 'package:flutter/material.dart';
// import 'package:flutterflow_ui/flutterflow_ui.dart';
// import 'package:vbvs_app/common/util/FitTool.dart';
// import 'package:vbvs_app/common/util/MyUtils.dart';
// import 'package:intl/intl.dart';
// class SnoreChartContainer extends StatelessWidget {
// final List<dynamic> snoreValues;
// final List<dynamic> barData;
// final List<dynamic> showLabel;
// final int startTime;
// final int endTime;
// const SnoreChartContainer({
// required this.snoreValues,
// required this.barData,
// required this.showLabel,
// required this.startTime,
// required this.endTime,
// super.key,
// });
// @override
// Widget build(BuildContext context) {
// return Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// SnoreBarOverlay(
// barData: barData,
// showLabel: showLabel,
// startTime: startTime,
// endTime: endTime,
// ),
// Container(height: 32.rpx),
// Container(
// height: 23.rpx,
// child: SnoreWaveform(
// snoreValues: snoreValues,
// startTime: startTime,
// endTime: endTime,
// ),
// ),
// ],
// );
// }
// }
// class SnoreBarOverlay extends StatelessWidget {
// final List<dynamic> barData;
// final List<dynamic> showLabel;
// final int startTime;
// final int endTime;
// const SnoreBarOverlay({
// required this.barData,
// required this.showLabel,
// required this.startTime,
// required this.endTime,
// super.key,
// });
// @override
// Widget build(BuildContext context) {
// const double barHeight = 50;
// return SizedBox(
// height: barHeight,
// child: CustomPaint(
// size: Size(double.infinity, barHeight),
// painter: SnoreBarPainter(
// barData: barData,
// showLabel: showLabel,
// startTime: startTime,
// endTime: endTime,
// ),
// ),
// );
// }
// }
// class SnoreBarPainter extends CustomPainter {
// final List<dynamic> barData;
// final List<dynamic> showLabel;
// final int startTime;
// final int endTime;
// SnoreBarPainter({
// required this.barData,
// required this.showLabel,
// required this.startTime,
// required this.endTime,
// });
// @override
// void paint(Canvas canvas, Size size) {
// final double width = size.width;
// final double height = size.height;
// final double pixelPerMs = width / (endTime - startTime);
// for (var item in barData) {
// final int st = item['st'];
// final int et = item['et'];
// final int type = item['type'];
// int heightInit = 1;
// final match = showLabel.firstWhere(
// (e) => e['type'] == type,
// orElse: () => null,
// );
// Color barColor = Colors.transparent;
// if (match != null) {
// final dynamic colorStr = match['color'];
// if (colorStr != null && colorStr.toString().isNotEmpty) {
// barColor = stringToColor(colorStr);
// }
// }
// final Paint barPaint = Paint()
// ..color = barColor
// ..style = PaintingStyle.fill;
// final double leftX = (st - startTime) * pixelPerMs;
// final double rightX = (et - startTime) * pixelPerMs;
// final double barWidth = rightX - leftX;
// //rem 深睡 中
// //浅睡 低
// //其他 高
// if (type == 1) {
// heightInit = 1;
// } else if (type == 2 || type == 6) {
// heightInit = 2;
// } else {
// heightInit = 3;
// }
// final double barHeight = (heightInit + 5).toDouble() * 8;
// final double top = height - barHeight;
// final rect = Rect.fromLTWH(leftX, top, barWidth, barHeight);
// canvas.drawRect(rect, barPaint);
// }
// }
// @override
// bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
// }
// class SnoreWaveform extends StatelessWidget {
// final List<dynamic> snoreValues;
// final int startTime;
// final int endTime;
// const SnoreWaveform({
// required this.snoreValues,
// required this.startTime,
// required this.endTime,
// super.key,
// });
// @override
// Widget build(BuildContext context) {
// return SizedBox(
// height: 150,
// child: LayoutBuilder(
// builder: (context, constraints) {
// return CustomPaint(
// size: Size(constraints.maxWidth, constraints.maxHeight),
// painter: SnoreWaveformPainter(
// snoreValues: snoreValues,
// startTime: startTime,
// endTime: endTime,
// ),
// );
// },
// ),
// );
// }
// }
// class SnoreWaveformPainter extends CustomPainter {
// final List<dynamic> snoreValues;
// final int startTime;
// final int endTime;
// SnoreWaveformPainter({
// required this.snoreValues,
// required this.startTime,
// required this.endTime,
// });
// @override
// void paint(Canvas canvas, Size size) {
// final double width = size.width;
// final double height = size.height;
// if (width <= 0 || height <= 0) return;
// final double totalDuration = (endTime - startTime).toDouble();
// if (totalDuration <= 0) return;
// final double pixelPerMs = width / totalDuration;
// // 过滤在时间范围内的有效事件
// final validEvents = snoreValues.where((e) {
// final int st = e['st'];
// final int et = e['et'];
// // 事件与时间范围有重叠
// return !(et <= startTime || st >= endTime);
// }).toList();
// // 计算中心线位置
// final double centerY = height / 2;
// // 统一使用一个颜色(打鼾颜色)
// final Color snoreColor = stringToColor("#8E7DEF").withOpacity(0.8);
// final Paint barPaint = Paint()
// ..color = snoreColor
// ..style = PaintingStyle.fill;
// final Paint borderPaint = Paint()
// ..color = snoreColor.withOpacity(0.9)
// ..style = PaintingStyle.stroke
// ..strokeWidth = 0.5;
// // 固定高度(上下对称)
// final double fixedBarHeight = height * 0.3; // 固定为画布高度的30%
// // 绘制每个打鼾事件(上下对称的柱状图)
// for (final event in validEvents) {
// final int st = event['st'];
// final int et = event['et'];
// // 计算绘制位置(裁剪到可视范围内)
// final double startX = (st - startTime) * pixelPerMs;
// final double endX = (et - startTime) * pixelPerMs;
// // 确保在画布范围内
// if (endX < 0 || startX > width) continue;
// final double drawStartX = startX.clamp(0, width);
// final double drawEndX = endX.clamp(0, width);
// final double drawWidth = drawEndX - drawStartX;
// if (drawWidth <= 0) continue;
// // 绘制上方的柱状图
// final double topBarTop = centerY - fixedBarHeight;
// final Rect topRect =
// Rect.fromLTWH(drawStartX, topBarTop, drawWidth, fixedBarHeight);
// canvas.drawRect(topRect, barPaint);
// canvas.drawRect(topRect, borderPaint);
// // 绘制下方的柱状图(对称)
// final double bottomBarTop = centerY;
// final Rect bottomRect =
// Rect.fromLTWH(drawStartX, bottomBarTop, drawWidth, fixedBarHeight);
// canvas.drawRect(bottomRect, barPaint);
// canvas.drawRect(bottomRect, borderPaint);
// }
// // 绘制中心线
// final Paint axisPaint = Paint()
// ..color = Colors.grey.withOpacity(0.3)
// ..strokeWidth = 0.5;
// canvas.drawLine(Offset(0, centerY), Offset(width, centerY), axisPaint);
// // 绘制时间轴标签
// final textPainter = TextPainter(
// textAlign: TextAlign.center,
// textDirection: ui.TextDirection.ltr,
// );
// final int hourMs = 60 * 60 * 1000;
// final int totalHours = (endTime - startTime) ~/ hourMs;
// // 创建开始和结束时间的DateTime对象
// final DateTime startDt = DateTime.fromMillisecondsSinceEpoch(startTime);
// final DateTime endDt = DateTime.fromMillisecondsSinceEpoch(endTime);
// // 1. 始终显示开始时间
// String label = DateFormat('HH:mm').format(startDt);
// textPainter.text = TextSpan(
// text: label,
// style: TextStyle(fontSize: 10, color: Colors.grey),
// );
// textPainter.layout();
// textPainter.paint(
// canvas,
// Offset(0 - textPainter.width / 2, height + 2),
// );
// // 2. 决定显示策略
// if (totalHours <= 8) {
// // 小时间段:显示所有整点小时(基于实际时间)
// DateTime currentHour = DateTime(
// startDt.year,
// startDt.month,
// startDt.day,
// startDt.hour + 1, // 从下一个整点开始
// 0,
// 0,
// 0,
// 0);
// // 如果开始时间本身就是整点,需要调整
// if (startDt.minute == 0 &&
// startDt.second == 0 &&
// startDt.millisecond == 0) {
// currentHour = startDt;
// }
// while (currentHour.millisecondsSinceEpoch < endTime) {
// int timeMs = currentHour.millisecondsSinceEpoch;
// // 确保时间在范围内
// if (timeMs > startTime && timeMs < endTime) {
// double x = (timeMs - startTime) * pixelPerMs;
// // 跳过太接近边界的时间点30分钟内不显示
// if (timeMs - startTime < 10 * 60 * 1000 ||
// endTime - timeMs < 10 * 60 * 1000) {
// currentHour = currentHour.add(Duration(hours: 1));
// continue;
// }
// label = "${currentHour.hour}";
// textPainter.text = TextSpan(
// text: label,
// style: TextStyle(fontSize: 10, color: Colors.grey),
// );
// textPainter.layout();
// textPainter.paint(
// canvas,
// Offset(x - textPainter.width / 2, height + 2),
// );
// }
// currentHour = currentHour.add(Duration(hours: 1));
// }
// } else {
// // 长时间段:使用自适应间隔
// int labelInterval = (totalHours / 6).ceil();
// // 计算第一个整点标签(对齐整点小时)
// DateTime firstLabelHour = DateTime(
// startDt.year,
// startDt.month,
// startDt.day,
// startDt.hour + (labelInterval - (startDt.hour % labelInterval)),
// 0,
// 0,
// 0,
// 0);
// // 如果第一个标签在开始时间之前,调整到下一个间隔
// if (firstLabelHour.millisecondsSinceEpoch <= startTime) {
// firstLabelHour = firstLabelHour.add(Duration(hours: labelInterval));
// }
// // 绘制中间标签
// DateTime currentHour = firstLabelHour;
// while (currentHour.millisecondsSinceEpoch < endTime) {
// int timeMs = currentHour.millisecondsSinceEpoch;
// // 跳过太接近边界的时间点1小时内不显示
// if (timeMs - startTime >= hourMs && endTime - timeMs >= hourMs) {
// double x = (timeMs - startTime) * pixelPerMs;
// label = "${currentHour.hour}";
// textPainter.text = TextSpan(
// text: label,
// style: TextStyle(fontSize: 10, color: Colors.grey),
// );
// textPainter.layout();
// textPainter.paint(
// canvas,
// Offset(x - textPainter.width / 2, height + 2),
// );
// }
// currentHour = currentHour.add(Duration(hours: labelInterval));
// }
// }
// // 3. 始终显示结束时间
// label = DateFormat('HH:mm').format(endDt);
// textPainter.text = TextSpan(
// text: label,
// style: TextStyle(fontSize: 10, color: Colors.grey),
// );
// textPainter.layout();
// textPainter.paint(
// canvas,
// Offset(width - textPainter.width / 2, height + 2),
// );
// }
// @override
// bool shouldRepaint(covariant SnoreWaveformPainter oldDelegate) {
// return oldDelegate.snoreValues != snoreValues ||
// oldDelegate.startTime != startTime ||
// oldDelegate.endTime != endTime;
// }
// }
import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:ef/ef.dart';

View File

@@ -25,63 +25,113 @@ class TimeSeriesChart extends StatelessWidget {
required this.dataPoints,
}) : super(key: key);
// X轴刻度数据
List<XLabel> _generateXLabels() {
final labels = <XLabel>[];
// 计算总分钟数
double get _totalMinutes {
return (endTime - startTime) / (1000 * 60);
}
// 生成X轴刻度标签
Map<double, String> _generateXLabels() {
final labels = <double, String>{};
final startDate = DateTime.fromMillisecondsSinceEpoch(startTime);
final endDate = DateTime.fromMillisecondsSinceEpoch(endTime);
// 第一个刻度,原始 startTimeHH:mm格式
labels.add(XLabel(
time: startTime,
label:
'${startDate.hour.toString().padLeft(2, '0')}:${startDate.minute.toString().padLeft(2, '0')}',
));
// 0分钟位置起始时间
labels[0.0] =
'${startDate.hour.toString().padLeft(2, '0')}:${startDate.minute.toString().padLeft(2, '0')}';
// 生成中间整点小时刻度,注意起点向上取整一个小时
DateTime current = DateTime(
startDate.year,
startDate.month,
startDate.day,
startDate.hour,
);
if (startDate.minute > 0 ||
startDate.second > 0 ||
startDate.millisecond > 0) {
// 如果 startTime 不是整点,跳到下一个整点小时
current = current.add(Duration(hours: 1));
// 计算总小时数
final int hourMs = 60 * 60 * 1000;
final int totalHours = (endTime - startTime) ~/ hourMs;
// 按照参考代码的逻辑当小时数超过8时跳着显示
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 minutesFromStart =
(currentHour.millisecondsSinceEpoch - startTime) / (1000 * 60);
labels[minutesFromStart] = '${currentHour.hour}';
}
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 minutesFromStart =
(currentHour.millisecondsSinceEpoch - startTime) / (1000 * 60);
labels[minutesFromStart] = '${currentHour.hour}';
}
currentHour = currentHour.add(Duration(hours: labelInterval));
}
}
while (current.isBefore(endDate)) {
labels.add(XLabel(
time: current.millisecondsSinceEpoch,
label: current.hour.toString(),
));
current = current.add(Duration(hours: 1));
}
// 最后一个刻度,原始 endTimeHH:mm格式
labels.add(XLabel(
time: endTime,
label:
'${endDate.hour.toString().padLeft(2, '0')}:${endDate.minute.toString().padLeft(2, '0')}',
));
// 最后位置:结束时间
labels[_totalMinutes] =
'${endDate.hour.toString().padLeft(2, '0')}:${endDate.minute.toString().padLeft(2, '0')}';
return labels;
}
// 时间戳映射到0~(labels.length-1)之间
double _timeToX(double timestamp, List<XLabel> labels) {
int start = labels.first.time;
int end = labels.last.time;
double total = (end - start).toDouble();
double pos = (timestamp - start).clamp(0, total).toDouble();
return pos / total * (labels.length - 1);
// 时间戳映射到X坐标分钟数
double _timeToX(double timestamp) {
final minutesFromStart = (timestamp - startTime) / (1000 * 60);
return minutesFromStart.clamp(0.0, _totalMinutes);
}
@override
Widget build(BuildContext context) {
final xLabels = _generateXLabels();
final labelPositions = xLabels.keys.toList()..sort();
final midY = (yMin + yMax) / 2;
// 将数据点分割成多个连续段遇到value=-1时断开
@@ -92,7 +142,7 @@ class TimeSeriesChart extends StatelessWidget {
if (point.value != -1) {
// 有效数据点,添加到当前段
currentSegment.add(FlSpot(
_timeToX(point.timestamp.toDouble(), xLabels),
_timeToX(point.timestamp.toDouble()),
point.value,
));
} else if (currentSegment.isNotEmpty) {
@@ -112,7 +162,7 @@ class TimeSeriesChart extends StatelessWidget {
child: LineChart(
LineChartData(
minX: 0,
maxX: (xLabels.length - 1).toDouble(),
maxX: _totalMinutes,
minY: yMin < 0 ? yMin : 0,
maxY: yMax,
gridData: FlGridData(show: false),
@@ -144,37 +194,26 @@ class TimeSeriesChart extends StatelessWidget {
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
interval: 1,
interval: 1, // 现在每个单位是1分钟
getTitlesWidget: (value, meta) {
int index = value.toInt();
if (index < 0 || index >= xLabels.length)
return const SizedBox.shrink();
// 四舍五入到最接近的整数
final roundedValue = value.roundToDouble();
final dateTime =
DateTime.fromMillisecondsSinceEpoch(xLabels[index].time);
if (index == 0 || index == xLabels.length - 1) {
// 开始和结束显示 HH:mm
final formatted =
'${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(formatted,
style: TextStyle(
color: themeController.currentColor.sc4,
fontSize: 16.rpx)),
);
} else {
// 中间显示小时H24小时制不补零
final formatted = '${dateTime.hour}';
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(formatted,
style: TextStyle(
color: themeController.currentColor.sc4,
fontSize: 16.rpx)),
);
// 检查是否在标签位置
for (var position in labelPositions) {
if ((position - roundedValue).abs() < 0.5) {
final label = xLabels[position] ?? '';
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(label,
style: TextStyle(
color: themeController.currentColor.sc4,
fontSize: 16.rpx)),
);
}
}
return const SizedBox.shrink();
},
),
),
@@ -238,9 +277,3 @@ class TimeSeriesChart extends StatelessWidget {
);
}
}
class XLabel {
final int time;
final String label;
XLabel({required this.time, required this.label});
}