Files
tuiche/lib/pages/sleep_report/chart/TimeLineChart.dart
2025-05-22 08:56:27 +08:00

274 lines
7.6 KiB
Dart

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'dart:ui' as ui;
import 'package:vbvs_app/common/util/MyUtils.dart';
class TimeLineChart extends StatelessWidget {
final List<DataPoint> points;
final double yMin;
final double yMax;
final int startTime;
final int endTime;
final double width;
final double height;
const TimeLineChart({
super.key,
required this.points,
required this.yMin,
required this.yMax,
required this.startTime,
required this.endTime,
this.width = 400,
this.height = 300,
});
@override
Widget build(BuildContext context) {
return CustomPaint(
size: Size(width, height),
painter: _TimeLineChartPainter(
points: points,
yMin: yMin,
yMax: yMax,
startTime: startTime,
endTime: endTime,
),
);
}
}
class DataPoint {
final int timestamp;
final double value;
DataPoint(this.timestamp, this.value);
}
class _TimeLineChartPainter extends CustomPainter {
final List<DataPoint> points;
final double yMin;
final double yMax;
final int startTime;
final int endTime;
_TimeLineChartPainter({
required this.points,
required this.yMin,
required this.yMax,
required this.startTime,
required this.endTime,
});
@override
void paint(Canvas canvas, Size size) {
_drawYAxis(canvas, size);
_drawXAxis(canvas, size);
_drawLine(canvas, size);
}
void _drawXAxis(Canvas canvas, Size size) {
const margin = 40.0;
final paint = Paint()..color = Colors.black;
final textStyle = const TextStyle(color: Colors.black, fontSize: 12);
// Draw X axis line
canvas.drawLine(
Offset(margin, size.height - margin),
Offset(size.width - margin, size.height - margin),
paint,
);
// Generate time ticks
final timeFormatStartEnd = DateFormat('HH:mm');
final timeFormatMiddle = DateFormat('h');
final startDateTime = DateTime.fromMillisecondsSinceEpoch(startTime);
final endDateTime = DateTime.fromMillisecondsSinceEpoch(endTime);
List<DateTime> hourTicks = [];
DateTime current = DateTime(
startDateTime.year,
startDateTime.month,
startDateTime.day,
startDateTime.hour,
).add(const Duration(hours: 1));
while (current.isBefore(endDateTime)) {
if (current.isAfter(startDateTime)) {
hourTicks.add(current);
}
current = current.add(const Duration(hours: 1));
}
void drawTick(DateTime time, bool isEdge) {
final x = margin +
((time.millisecondsSinceEpoch - startTime) / (endTime - startTime)) *
(size.width - 2 * margin);
final text = isEdge
? timeFormatStartEnd.format(time)
: timeFormatMiddle.format(time);
_drawText(
canvas,
text,
Offset(x, size.height - margin + 20),
TextAlign.center,
);
}
drawTick(startDateTime, true);
drawTick(endDateTime, true);
for (var tick in hourTicks) {
drawTick(tick, false);
}
}
void _drawYAxis(Canvas canvas, Size size) {
const margin = 40.0;
final midValue = (yMax + yMin) / 2;
// 计算三条虚线之间的垂直间距
final lineSpacing = (size.height - 2 * margin) / 3; // 让三条线之间的间距相等
// 新增的 y=0 实线的垂直位置
final zeroLinePosition = margin + lineSpacing * 3; // 确保 y=0 位于三条虚线下方
void drawLine(double value, Color color,
{bool isDashed = false, bool isSolid = false}) {
final y =
(value - yMax) / (yMin - yMax) * (size.height - 2 * margin) + margin;
final path = Path();
path.moveTo(margin, y);
path.lineTo(size.width - margin, y);
final paint = Paint()
..color = color
..strokeWidth = (color != Colors.grey) ? 2 : 1
..style = PaintingStyle.stroke;
if (isDashed) {
Path dashedPath = _createDashedPath(path, dashWidth: 5, dashSpace: 5);
canvas.drawPath(dashedPath, paint);
} else if (isSolid) {
// 对于实线,直接绘制
canvas.drawPath(path, paint);
} else {
// 默认使用虚线绘制
canvas.drawPath(path, paint);
}
}
// 绘制 y=0 的灰色实线,并将其放置在三条虚线的下方
if (yMin < 0 && yMax > 0) {
drawLine(0, Colors.grey, isSolid: true); // 灰色实线绘制 y=0 线
}
// 绘制最小值、中间值、最大值的虚线
drawLine(yMin, themeController.currentColor.sc9, isDashed: true);
drawLine(midValue, themeController.currentColor.sc4, isDashed: true);
drawLine(yMax, themeController.currentColor.sc9, isDashed: true);
}
Path _createDashedPath(Path path,
{required double dashWidth, required double dashSpace}) {
final Path dashedPath = Path();
final ui.PathMetrics metrics = path.computeMetrics();
for (ui.PathMetric metric in metrics) {
double distance = 0;
while (distance < metric.length) {
dashedPath.addPath(
metric.extractPath(distance, distance + dashWidth),
Offset.zero,
);
distance += dashWidth + dashSpace;
}
}
return dashedPath;
}
void _drawLine(Canvas canvas, Size size) {
const margin = 40.0;
final sortedPoints = points
..sort((a, b) => a.timestamp.compareTo(b.timestamp));
Path? currentPath;
Paint currentPaint = _createPaint(Colors.green);
for (int i = 0; i < sortedPoints.length - 1; i++) {
final p1 = sortedPoints[i];
final p2 = sortedPoints[i + 1];
final x1 = margin +
((p1.timestamp - startTime) / (endTime - startTime)) *
(size.width - 2 * margin);
final y1 = margin +
(1 - (p1.value - yMin) / (yMax - yMin)) * (size.height - 2 * margin);
final x2 = margin +
((p2.timestamp - startTime) / (endTime - startTime)) *
(size.width - 2 * margin);
final y2 = margin +
(1 - (p2.value - yMin) / (yMax - yMin)) * (size.height - 2 * margin);
final shouldBeGreen = p1.value >= yMin &&
p1.value <= yMax &&
p2.value >= yMin &&
p2.value <= yMax;
// 根据当前线段的状态来决定是否切换颜色和虚线状态
if (shouldBeGreen != (currentPaint.color == Colors.green)) {
if (currentPath != null) {
canvas.drawPath(currentPath, currentPaint);
}
currentPath = Path();
currentPaint = _createPaint(shouldBeGreen ? Colors.green : Colors.red);
}
currentPath ??= Path();
if (i == 0) currentPath.moveTo(x1, y1);
currentPath.lineTo(x2, y2);
}
// 绘制剩余路径
if (currentPath != null) {
if (currentPaint.color == Colors.red) {
// 如果是红色线,绘制虚线
final dashedPath =
_createDashedPath(currentPath, dashWidth: 5, dashSpace: 5);
canvas.drawPath(dashedPath, currentPaint);
} else {
canvas.drawPath(currentPath, currentPaint);
}
}
}
Paint _createPaint(Color color) => Paint()
..color = color
..strokeWidth = 2
..style = PaintingStyle.stroke;
void _drawText(Canvas canvas, String text, Offset offset, TextAlign align) {
final textPainter = TextPainter(
text: TextSpan(
text: text,
style: const TextStyle(color: Colors.black, fontSize: 12),
),
textDirection: ui.TextDirection.ltr,
)..layout();
final centeredOffset = offset.translate(
-textPainter.width / 2,
-textPainter.height / 2,
);
textPainter.paint(canvas, centeredOffset);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}