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

253 lines
7.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
import 'dart:ui' as ui;
import 'dart:math';
class LineChartByRange extends StatelessWidget {
final List<Map<String, dynamic>> showLabel;
final int startTime;
final int endTime;
const LineChartByRange({
Key? key,
required this.showLabel,
required this.startTime,
required this.endTime,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (showLabel.isEmpty) return const SizedBox();
int maxTimes =
showLabel.map((e) => e['times'] ?? 0).reduce((a, b) => a > b ? a : b);
int yMax = (maxTimes / 10).ceil() * 10;
if (yMax == 0) yMax = 10;
DateTime minTime = DateTime.fromMillisecondsSinceEpoch(startTime);
DateTime maxTime = DateTime.fromMillisecondsSinceEpoch(endTime);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 500.rpx,
child: CustomPaint(
size: Size(double.infinity, 500.rpx),
painter: _LineChartByRangePainter(
data: showLabel,
yMax: yMax,
minTime: minTime,
maxTime: maxTime,
),
),
),
],
);
}
}
class _LineChartByRangePainter extends CustomPainter {
final List<Map<String, dynamic>> data;
final int yMax;
final DateTime minTime;
final DateTime maxTime;
_LineChartByRangePainter({
required this.data,
required this.yMax,
required this.minTime,
required this.maxTime,
});
@override
void paint(Canvas canvas, Size size) {
double padding = 20.rpx;
double labelInset = 12.rpx; // X轴标签缩进距离
// 绘图X轴起止点考虑内缩labelInset
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 linePaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 3.rpx
..color = stringToColor("#00C1AA")
..strokeCap = StrokeCap.round;
Paint fillCirclePaint = Paint()
..style = PaintingStyle.fill
..color = stringToColor("#00C1AA");
// 1. 先绘制数据线段及起止点圆点
for (var item in data) {
int start = item['startTime'];
int end = item['endTime'];
int times = item['times'];
double startX = xStart +
chartWidth * (start - minTime.millisecondsSinceEpoch) / totalDuration;
double endX = xStart +
chartWidth * (end - minTime.millisecondsSinceEpoch) / totalDuration;
double y = chartHeight * (1 - times / yMax);
// 画线段
canvas.drawLine(Offset(startX, y), Offset(endX, y), linePaint);
// 画起点圆点
canvas.drawCircle(Offset(startX, y), 4.rpx, fillCirclePaint);
// 画终点圆点
canvas.drawCircle(Offset(endX, y), 4.rpx, fillCirclePaint);
}
// 2. Y轴辅助线及文字
Paint axisPaint = Paint()
..color = Colors.grey.withOpacity(0.4)
..strokeWidth = 1.rpx;
for (int i = 0; i <= 6; i++) {
double y = chartHeight * i / 6;
if (i == 6) {
// 实线
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,
);
}
// Y轴文字
TextPainter tp = TextPainter(
text: TextSpan(
text: '${yMax - (yMax * i / 6).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));
}
// 3. X轴线
canvas.drawLine(
Offset(xStart, chartHeight), Offset(xEnd, chartHeight), axisPaint);
// 4. 画X轴时间点对应的垂直虚线辅助线
int totalHours = maxTime.difference(minTime).inHours;
int startHour = minTime.hour;
// for (int i = 1; i < totalHours; i++) {
// double x = xStart + chartWidth * i / totalHours;
// // 垂直虚线
// drawDashedLine(
// canvas,
// Offset(x, 0),
// Offset(x, chartHeight),
// axisPaint,
// dashWidth: 4.rpx,
// dashSpace: 4.rpx,
// );
// }
// 5. 画左侧完整时分 (HH:mm),往内缩 labelInset
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));
// 6. 画右侧完整时分 (HH:mm),往内缩 labelInset
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));
// 7. 中间小时数字(23, 0, 1, 2, ...)
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;
}
}
}