Files
tuiche/lib/pages/sleep_report/chart/SnoreChart.dart
2025-07-17 10:06:13 +08:00

285 lines
8.3 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:ui' as ui;
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:vbvs_app/common/color/appConstants.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
class BarData {
final int st; // 起始时间(毫秒)
final int et; // 结束时间(毫秒)
final double value; // 柱子高度
final int id;
final String name;
final Color color;
BarData({
required this.st,
required this.et,
required this.value,
required this.id,
required this.name,
required this.color,
});
}
class BarChartWidget extends StatefulWidget {
final List<BarData> data;
final int startTime; // 毫秒时间戳
final int endTime; // 毫秒时间戳
final double maxYValue; // Y轴最大值
final int yStepCount; // Y轴分段数
const BarChartWidget({
super.key,
required this.data,
required this.startTime,
required this.endTime,
required this.maxYValue,
this.yStepCount = 5,
});
@override
State<BarChartWidget> createState() => _BarChartWidgetState();
}
class _BarChartWidgetState extends State<BarChartWidget> {
BarData? selectedBar;
void _handleTapOrDrag(Offset localPosition, Size size) {
final chartWidth = size.width - 30.rpx;
final totalDuration = widget.endTime - widget.startTime;
for (final d in widget.data) {
final left =
((d.st - widget.startTime) / totalDuration) * chartWidth + 30.rpx;
final right =
((d.et - widget.startTime) / totalDuration) * chartWidth + 30.rpx;
if (localPosition.dx >= left && localPosition.dx <= right) {
setState(() {
selectedBar = d;
});
return;
}
}
setState(() {
selectedBar = null;
});
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (details) =>
_handleTapOrDrag(details.localPosition, constraints.biggest),
onPanUpdate: (details) =>
_handleTapOrDrag(details.localPosition, constraints.biggest),
onTapDown: (details) =>
_handleTapOrDrag(details.localPosition, constraints.biggest),
child: CustomPaint(
size: Size(double.infinity, 500.rpx),
painter: BarChartPainter(
widget.data,
widget.startTime,
widget.endTime,
maxYValue: widget.maxYValue,
yStepCount: widget.yStepCount,
selectedBar: selectedBar,
),
),
);
});
}
}
class BarChartPainter extends CustomPainter {
final List<BarData> data;
final int startTime;
final int endTime;
final double maxYValue;
final int yStepCount;
final BarData? selectedBar;
final double topPadding = 0;
final double bottomPadding = 0;
final double leftPadding = 30.rpx;
BarChartPainter(
this.data,
this.startTime,
this.endTime, {
required this.maxYValue,
this.yStepCount = 5,
this.selectedBar,
});
@override
void paint(Canvas canvas, Size size) {
final chartWidth = size.width - leftPadding;
final chartHeight = size.height - topPadding - bottomPadding;
final totalDuration = endTime - startTime;
final textPainter = TextPainter(textDirection: ui.TextDirection.ltr);
final stepValue = maxYValue / yStepCount;
// Y轴刻度
for (int i = 0; i <= yStepCount; i++) {
final value = stepValue * i;
final y = topPadding + chartHeight - (value / maxYValue) * chartHeight;
final dashPaint = Paint()
..color = Colors.grey.withOpacity(0.5)
..strokeWidth = 0.5;
drawDashedLine(
canvas, Offset(leftPadding, y), Offset(size.width, y), dashPaint);
textPainter.text = TextSpan(
text: value.toStringAsFixed(0),
style: TextStyle(
fontSize: 20.rpx,
color: themeController.currentColor.sc4,
),
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(leftPadding - textPainter.width - 4, y - textPainter.height / 2),
);
}
// X轴刻度
final startDate = DateTime.fromMillisecondsSinceEpoch(startTime);
final endDate = DateTime.fromMillisecondsSinceEpoch(endTime);
final hourStep = const Duration(hours: 1);
final xAxisY = topPadding + chartHeight;
for (DateTime t = startDate; t.isBefore(endDate); t = t.add(hourStep)) {
final x = ((t.millisecondsSinceEpoch - startTime) / totalDuration) *
chartWidth +
leftPadding;
final timeLabel = (t == startDate || t == endDate)
? DateFormat('HH:mm').format(t)
: DateFormat('h').format(t);
textPainter.text = TextSpan(
text: timeLabel,
style: TextStyle(
fontSize: AppConstants().smaller_text_fontSize,
color: themeController.currentColor.sc4,
),
);
textPainter.layout();
textPainter.paint(canvas, Offset(x - textPainter.width / 2, xAxisY + 4));
}
// 额外终点标签
final endX =
((endTime - startTime) / totalDuration) * chartWidth + leftPadding;
final endLabel = DateFormat('HH:mm').format(endDate);
textPainter.text = TextSpan(
text: endLabel,
style: TextStyle(
fontSize: AppConstants().smaller_text_fontSize,
color: themeController.currentColor.sc4,
),
);
textPainter.layout();
textPainter.paint(canvas, Offset(endX - textPainter.width / 2, xAxisY + 4));
// 柱子绘制 & 提示信息缓存
Offset? tipOffset;
Size? tipSize;
String? tipText;
Color tipColor = Colors.black;
for (final d in data) {
final left =
((d.st - startTime) / totalDuration) * chartWidth + leftPadding;
final right =
((d.et - startTime) / totalDuration) * chartWidth + leftPadding;
final barHeight = (d.value / maxYValue) * chartHeight;
final top = topPadding + chartHeight - barHeight;
final barPaint = Paint()..color = d.color;
canvas.drawRect(
Rect.fromLTRB(left, top, right, topPadding + chartHeight),
barPaint,
);
// 缓存 tip 信息
if (selectedBar == d) {
tipText =
'${d.name}\n${d.value.toStringAsFixed(1)}\n${MyUtils.formatToHHmm(d.st)}';
final tp = TextPainter(
text: TextSpan(
text: tipText,
style: TextStyle(fontSize: 16.rpx, color: Colors.white),
),
textAlign: TextAlign.center,
textDirection: ui.TextDirection.ltr,
);
tp.layout();
final tipWidth = tp.width + 20.rpx;
final tipHeight = tp.height + 10.rpx;
final tipLeft = left + (right - left) / 2 - tipWidth / 2;
final tipTop = top - tipHeight - 10.rpx;
tipOffset = Offset(tipLeft, tipTop);
tipSize = Size(tipWidth, tipHeight);
tipColor = Colors.black.withOpacity(0.8);
}
}
// 绘制 tip在柱子之上
if (tipText != null && tipOffset != null && tipSize != null) {
final rect = RRect.fromRectAndRadius(
Rect.fromLTWH(
tipOffset.dx, tipOffset.dy, tipSize.width, tipSize.height),
Radius.circular(8.rpx),
);
final tipBgPaint = Paint()..color = tipColor;
canvas.drawRRect(rect, tipBgPaint);
final tipTextPainter = TextPainter(
text: TextSpan(
text: tipText,
style: TextStyle(fontSize: 16.rpx, color: Colors.white),
),
textAlign: TextAlign.center,
textDirection: ui.TextDirection.ltr,
);
tipTextPainter.layout();
tipTextPainter.paint(
canvas,
Offset(tipOffset.dx + 10.rpx, tipOffset.dy + 5.rpx),
);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
void drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint,
{double dashWidth = 5, double dashSpace = 3}) {
double totalLength = (end.dx - start.dx).abs();
double dashCount = (totalLength / (dashWidth + dashSpace)).floorToDouble();
double dx = start.dx;
final dy = start.dy;
for (int i = 0; i < dashCount; i++) {
final from = Offset(dx, dy);
final to = Offset(dx + dashWidth, dy);
canvas.drawLine(from, to, paint);
dx += dashWidth + dashSpace;
}
}
}