Files
tuiche/lib/pages/sleep_report/chart/LineChartByRange.dart
2025-11-21 10:34:18 +08:00

357 lines
10 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 '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'];
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;
}
}
}