更新
This commit is contained in:
273
lib/pages/sleep_report/chart/TimeLineChart.dart
Normal file
273
lib/pages/sleep_report/chart/TimeLineChart.dart
Normal file
@@ -0,0 +1,273 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user