274 lines
7.6 KiB
Dart
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;
|
|
}
|