Files
tuiche/lib/pages/sleep_report/chart/SnoreWaveform.dart
2025-08-16 17:45:55 +08:00

601 lines
17 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:flutterflow_ui/flutterflow_ui.dart';
// import 'package:vbvs_app/common/util/FitTool.dart';
// import 'package:vbvs_app/common/util/MyUtils.dart';
// import 'package:intl/intl.dart';
// class SnoreChartContainer extends StatelessWidget {
// final List<dynamic> snoreValues;
// final List<dynamic> barData;
// final List<dynamic> showLabel;
// final int startTime;
// final int endTime;
// const SnoreChartContainer({
// required this.snoreValues,
// required this.barData,
// required this.showLabel,
// required this.startTime,
// required this.endTime,
// super.key,
// });
// @override
// Widget build(BuildContext context) {
// return Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// SnoreBarOverlay(
// barData: barData,
// showLabel: showLabel,
// startTime: startTime,
// endTime: endTime,
// ),
// Container(height: 32.rpx),
// Container(
// height: 23.rpx,
// child: SnoreWaveform(
// snoreValues: snoreValues,
// startTime: startTime,
// endTime: endTime,
// ),
// ),
// ],
// );
// }
// }
// class SnoreBarOverlay extends StatelessWidget {
// final List<dynamic> barData;
// final List<dynamic> showLabel; // ✅ 加入
// final int startTime;
// final int endTime;
// const SnoreBarOverlay({
// required this.barData,
// required this.showLabel, // ✅ 加入
// required this.startTime,
// required this.endTime,
// super.key,
// });
// @override
// Widget build(BuildContext context) {
// const double barHeight = 50;
// return SizedBox(
// height: barHeight,
// child: CustomPaint(
// size: Size(double.infinity, barHeight),
// painter: SnoreBarPainter(
// barData: barData,
// showLabel: showLabel, // ✅ 加入
// startTime: startTime,
// endTime: endTime,
// ),
// ),
// );
// }
// }
// class SnoreBarPainter extends CustomPainter {
// final List<dynamic> barData;
// final List<dynamic> showLabel; // ✅ 加入
// final int startTime;
// final int endTime;
// SnoreBarPainter({
// required this.barData,
// required this.showLabel, // ✅ 加入
// required this.startTime,
// required this.endTime,
// });
// @override
// void paint(Canvas canvas, Size size) {
// final double width = size.width;
// final double height = size.height;
// final double pixelPerMs = width / (endTime - startTime);
// for (var item in barData) {
// final int st = item['st'];
// final int et = item['et'];
// final int type = item['type'];
// // 查找匹配的颜色
// final match = showLabel.firstWhere(
// (e) => e['type'] == type,
// orElse: () => null,
// );
// Color barColor = Colors.transparent;
// if (match != null) {
// final dynamic colorStr = match['color'];
// if (colorStr != null && colorStr.toString().isNotEmpty) {
// barColor = stringToColor(colorStr);
// }
// }
// final Paint barPaint = Paint()
// ..color = barColor
// ..style = PaintingStyle.fill;
// final double leftX = (st - startTime) * pixelPerMs;
// final double rightX = (et - startTime) * pixelPerMs;
// final double barWidth = rightX - leftX;
// final double barHeight = (type + 5).toDouble() * 8;
// final double top = height - barHeight;
// final rect = Rect.fromLTWH(leftX, top, barWidth, barHeight);
// canvas.drawRect(rect, barPaint);
// }
// }
// @override
// bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
// }
// class SnoreWaveform extends StatelessWidget {
// final List<dynamic> snoreValues;
// final int startTime;
// final int endTime;
// const SnoreWaveform({
// required this.snoreValues,
// required this.startTime,
// required this.endTime,
// super.key,
// });
// @override
// Widget build(BuildContext context) {
// return SizedBox(
// height: 150,
// child: CustomPaint(
// size: Size(double.infinity, 150),
// painter: SnoreWaveformPainter(
// snoreValues: snoreValues,
// startTime: startTime,
// endTime: endTime,
// ),
// ),
// );
// }
// }
// class SnoreWaveformPainter extends CustomPainter {
// final List<dynamic> snoreValues;
// final int startTime;
// final int endTime;
// SnoreWaveformPainter({
// required this.snoreValues,
// required this.startTime,
// required this.endTime,
// });
// @override
// void paint(Canvas canvas, Size size) {
// final double width = size.width;
// final double height = size.height;
// final double centerY = height / 2;
// final double totalDuration = (endTime - startTime).toDouble();
// final double pixelPerMs = width / totalDuration;
// final Paint wavePaint = Paint()
// ..color = stringToColor("#8E7DEF").withOpacity(0.8)
// ..strokeWidth = 1.5
// ..style = PaintingStyle.stroke;
// final Path upperPath = Path();
// final Path lowerPath = Path();
// // ✅ 获取最大值用于自适应比例
// double maxValue = snoreValues.fold<double>(0, (prev, e) {
// final value = e["value"]?.toDouble() ?? 0;
// return value > prev ? value : prev;
// });
// // ✅ 自适应缩放比例,限制波形最大高度为 height * 0.45
// final double maxWaveHeight = height * 1;
// final double scaleY = maxValue > 0 ? (maxWaveHeight / maxValue) : 1;
// for (int i = 0; i < snoreValues.length; i++) {
// final timestamp = snoreValues[i]["st"];
// final value = snoreValues[i]["value"]?.toDouble() ?? 0;
// final x = (timestamp - startTime) * pixelPerMs;
// final y = centerY - value * scaleY;
// final yMirror = centerY + value * scaleY;
// if (i == 0) {
// upperPath.moveTo(x, y);
// lowerPath.moveTo(x, yMirror);
// } else {
// upperPath.lineTo(x, y);
// lowerPath.lineTo(x, yMirror);
// }
// }
// canvas.drawPath(upperPath, wavePaint);
// canvas.drawPath(lowerPath, wavePaint);
// // ✅ 最后绘制中心线,防止被覆盖
// final Paint axisPaint = Paint()
// ..color = Colors.grey.withOpacity(0.6)
// ..strokeWidth = 0.5;
// canvas.drawLine(Offset(0, centerY), Offset(width, centerY), axisPaint);
// // ✅ 时间刻度绘制
// final textPainter = TextPainter(
// textAlign: TextAlign.center,
// textDirection: ui.TextDirection.ltr,
// );
// final int hourMs = 60 * 60 * 1000;
// for (int t = startTime; t < endTime; t += hourMs) {
// double x = (t - startTime) * pixelPerMs;
// DateTime dt = DateTime.fromMillisecondsSinceEpoch(t);
// String label = t == startTime
// ? DateFormat('HH:mm').format(dt)
// : DateFormat('h').format(dt); // 12小时制
// textPainter.text = TextSpan(
// text: label,
// style: TextStyle(fontSize: 10, color: Colors.grey),
// );
// textPainter.layout();
// textPainter.paint(
// canvas,
// Offset(x - textPainter.width / 2, height + 2), // 标签显示在底部
// );
// }
// // ✅ 画终点时间
// {
// double x = (endTime - startTime) * pixelPerMs;
// DateTime dt = DateTime.fromMillisecondsSinceEpoch(endTime);
// String label = DateFormat('HH:mm').format(dt);
// textPainter.text = TextSpan(
// text: label,
// style: TextStyle(fontSize: 10, color: Colors.grey),
// );
// textPainter.layout();
// textPainter.paint(
// canvas,
// Offset(x - textPainter.width / 2, height + 2),
// );
// }
// }
// @override
// bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
// }
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutterflow_ui/flutterflow_ui.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
import 'package:intl/intl.dart';
class SnoreChartContainer extends StatelessWidget {
final List<dynamic> snoreValues;
final List<dynamic> barData;
final List<dynamic> showLabel;
final int startTime;
final int endTime;
const SnoreChartContainer({
required this.snoreValues,
required this.barData,
required this.showLabel,
required this.startTime,
required this.endTime,
super.key,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SnoreBarOverlay(
barData: barData,
showLabel: showLabel,
startTime: startTime,
endTime: endTime,
),
Container(height: 32.rpx),
Container(
height: 23.rpx,
child: SnoreWaveform(
snoreValues: snoreValues,
startTime: startTime,
endTime: endTime,
),
),
],
);
}
}
class SnoreBarOverlay extends StatelessWidget {
final List<dynamic> barData;
final List<dynamic> showLabel;
final int startTime;
final int endTime;
const SnoreBarOverlay({
required this.barData,
required this.showLabel,
required this.startTime,
required this.endTime,
super.key,
});
@override
Widget build(BuildContext context) {
const double barHeight = 50;
return SizedBox(
height: barHeight,
child: CustomPaint(
size: Size(double.infinity, barHeight),
painter: SnoreBarPainter(
barData: barData,
showLabel: showLabel,
startTime: startTime,
endTime: endTime,
),
),
);
}
}
class SnoreBarPainter extends CustomPainter {
final List<dynamic> barData;
final List<dynamic> showLabel;
final int startTime;
final int endTime;
SnoreBarPainter({
required this.barData,
required this.showLabel,
required this.startTime,
required this.endTime,
});
@override
void paint(Canvas canvas, Size size) {
final double width = size.width;
final double height = size.height;
final double pixelPerMs = width / (endTime - startTime);
for (var item in barData) {
final int st = item['st'];
final int et = item['et'];
final int type = item['type'];
final match = showLabel.firstWhere(
(e) => e['type'] == type,
orElse: () => null,
);
Color barColor = Colors.transparent;
if (match != null) {
final dynamic colorStr = match['color'];
if (colorStr != null && colorStr.toString().isNotEmpty) {
barColor = stringToColor(colorStr);
}
}
final Paint barPaint = Paint()
..color = barColor
..style = PaintingStyle.fill;
final double leftX = (st - startTime) * pixelPerMs;
final double rightX = (et - startTime) * pixelPerMs;
final double barWidth = rightX - leftX;
final double barHeight = (type + 5).toDouble() * 8;
final double top = height - barHeight;
final rect = Rect.fromLTWH(leftX, top, barWidth, barHeight);
canvas.drawRect(rect, barPaint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
class SnoreWaveform extends StatelessWidget {
final List<dynamic> snoreValues;
final int startTime;
final int endTime;
const SnoreWaveform({
required this.snoreValues,
required this.startTime,
required this.endTime,
super.key,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 150,
child: CustomPaint(
size: Size(double.infinity, 150),
painter: SnoreWaveformPainter(
snoreValues: snoreValues,
startTime: startTime,
endTime: endTime,
),
),
);
}
}
class SnoreWaveformPainter extends CustomPainter {
final List<dynamic> snoreValues;
final int startTime;
final int endTime;
SnoreWaveformPainter({
required this.snoreValues,
required this.startTime,
required this.endTime,
});
@override
void paint(Canvas canvas, Size size) {
final double width = size.width;
final double height = size.height;
final double centerY = height / 2;
final double totalDuration = (endTime - startTime).toDouble();
final double pixelPerMs = width / totalDuration;
final Paint wavePaint = Paint()
..color = stringToColor("#8E7DEF").withOpacity(0.8)
..strokeWidth = 1.5
..style = PaintingStyle.stroke;
final Path upperPath = Path();
final Path lowerPath = Path();
double maxValue = snoreValues.fold<double>(0, (prev, e) {
final value = e["value"]?.toDouble() ?? 0;
return value > prev ? value : prev;
});
final double maxWaveHeight = height * 1;
final double scaleY = maxValue > 0 ? (maxWaveHeight / maxValue) : 1;
for (int i = 0; i < snoreValues.length; i++) {
final timestamp = snoreValues[i]["st"];
final value = snoreValues[i]["value"]?.toDouble() ?? 0;
final x = (timestamp - startTime) * pixelPerMs;
final y = centerY - value * scaleY;
final yMirror = centerY + value * scaleY;
if (i == 0) {
upperPath.moveTo(x, y);
lowerPath.moveTo(x, yMirror);
} else {
upperPath.lineTo(x, y);
lowerPath.lineTo(x, yMirror);
}
}
canvas.drawPath(upperPath, wavePaint);
canvas.drawPath(lowerPath, wavePaint);
final Paint axisPaint = Paint()
..color = Colors.grey.withOpacity(0.6)
..strokeWidth = 0.5;
canvas.drawLine(Offset(0, centerY), Offset(width, centerY), axisPaint);
final textPainter = TextPainter(
textAlign: TextAlign.center,
textDirection: ui.TextDirection.ltr,
);
final int hourMs = 60 * 60 * 1000;
final int totalHours = (endTime - startTime) ~/ hourMs;
// 1. 始终显示开始时间
double x = 0;
DateTime startDt = DateTime.fromMillisecondsSinceEpoch(startTime);
String label = DateFormat('HH:mm').format(startDt);
textPainter.text = TextSpan(
text: label,
style: TextStyle(fontSize: 10, color: Colors.grey),
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(x - textPainter.width / 2, height + 2),
);
// 2. 决定显示策略
if (totalHours <= 8) {
// 小时间段:显示所有整点小时
for (int t = startTime + hourMs; t < endTime; t += hourMs) {
x = (t - startTime) * pixelPerMs;
DateTime dt = DateTime.fromMillisecondsSinceEpoch(t);
// 判断是否接近边界30分钟内不显示
if (t - startTime < 30 * 60 * 1000 || endTime - t < 30 * 60 * 1000) {
continue;
}
label = dt.hour == 0 ? "0" : "${dt.hour}";
textPainter.text = TextSpan(
text: label,
style: TextStyle(fontSize: 10, color: Colors.grey),
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(x - textPainter.width / 2, height + 2),
);
}
} else {
// 长时间段:使用自适应间隔
int labelInterval = (totalHours / 6).ceil();
// 计算第一个标签位置(对齐整点)
int firstLabelMs =
((startTime ~/ (labelInterval * hourMs))) * labelInterval * hourMs;
if (firstLabelMs <= startTime) {
firstLabelMs += labelInterval * hourMs;
}
// 绘制中间标签
for (int t = firstLabelMs; t < endTime; t += labelInterval * hourMs) {
// 跳过太接近边界的时间点1小时内不显示
if (t - startTime < hourMs || endTime - t < hourMs) continue;
x = (t - startTime) * pixelPerMs;
DateTime dt = DateTime.fromMillisecondsSinceEpoch(t);
label = dt.hour == 0 ? "0" : "${dt.hour}";
textPainter.text = TextSpan(
text: label,
style: TextStyle(fontSize: 10, color: Colors.grey),
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(x - textPainter.width / 2, height + 2),
);
}
}
// 3. 始终显示结束时间
x = (endTime - startTime) * pixelPerMs;
DateTime endDt = DateTime.fromMillisecondsSinceEpoch(endTime);
label = DateFormat('HH:mm').format(endDt);
textPainter.text = TextSpan(
text: label,
style: TextStyle(fontSize: 10, color: Colors.grey),
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(x - textPainter.width / 2, height + 2),
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}