更新睡眠报告
This commit is contained in:
@@ -50,9 +50,7 @@ class FatigueCircleIndicator extends StatelessWidget {
|
||||
'$percent%',
|
||||
style: TextStyle(
|
||||
fontSize: AppConstants().normal_text_fontSize,
|
||||
color: percent > 60
|
||||
? themeController.currentColor.sc9
|
||||
: themeController.currentColor.sc3,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.rpx),
|
||||
@@ -60,9 +58,7 @@ class FatigueCircleIndicator extends StatelessWidget {
|
||||
explain,
|
||||
style: TextStyle(
|
||||
fontSize: AppConstants().normal_text_fontSize,
|
||||
color: percent > 60
|
||||
? themeController.currentColor.sc9
|
||||
: themeController.currentColor.sc3,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -5,69 +5,517 @@ import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
import 'dart:ui' as ui;
|
||||
import 'dart:math';
|
||||
|
||||
class LineChartByRange extends StatelessWidget {
|
||||
//根据数据自定义
|
||||
// class LineChartByRange extends StatefulWidget {
|
||||
// final List<Map<String, dynamic>> showLabel;
|
||||
// final int startTime;
|
||||
// final int endTime;
|
||||
// final int? threshold;
|
||||
|
||||
// const LineChartByRange({
|
||||
// Key? key,
|
||||
// required this.showLabel,
|
||||
// required this.startTime,
|
||||
// required this.endTime,
|
||||
// this.threshold, // 新增
|
||||
// }) : 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();
|
||||
|
||||
// int maxTimes = widget.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(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 / yMax);
|
||||
|
||||
// // 判断点击范围(圆点半径±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,
|
||||
// yMax: yMax,
|
||||
// minTime: minTime,
|
||||
// maxTime: maxTime,
|
||||
// threshold: widget.threshold, // 新增
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// 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'
|
||||
// '次数: ${selectedData!['times']}',
|
||||
// style: TextStyle(
|
||||
// fontSize: 18.rpx,
|
||||
// color: Colors.white,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// class _LineChartByRangePainter extends CustomPainter {
|
||||
// final List<Map<String, dynamic>> data;
|
||||
// final int yMax;
|
||||
// final DateTime minTime;
|
||||
// final DateTime maxTime;
|
||||
// final int? threshold;
|
||||
|
||||
// _LineChartByRangePainter({
|
||||
// required this.data,
|
||||
// required this.yMax,
|
||||
// required this.minTime,
|
||||
// required this.maxTime,
|
||||
// this.threshold,
|
||||
// });
|
||||
|
||||
// @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 linePaint = Paint()
|
||||
// ..style = PaintingStyle.stroke
|
||||
// ..strokeWidth = 3.rpx
|
||||
// ..color = stringToColor("#00C1AA")
|
||||
// ..strokeCap = StrokeCap.round;
|
||||
|
||||
// Paint axisPaint = Paint()
|
||||
// ..color = Colors.grey.withOpacity(0.4)
|
||||
// ..strokeWidth = 1.rpx;
|
||||
|
||||
// Paint thresholdPaint = Paint()
|
||||
// ..color = themeController.currentColor.sc9
|
||||
// ..strokeWidth = 1.rpx;
|
||||
|
||||
// // 1. 阈值虚线(红色)
|
||||
// if (threshold != null && threshold! >= 0 && threshold! <= yMax) {
|
||||
// double yThreshold = chartHeight * (1 - threshold! / yMax);
|
||||
// drawDashedLine(
|
||||
// canvas,
|
||||
// Offset(xStart, yThreshold),
|
||||
// Offset(xEnd, yThreshold),
|
||||
// thresholdPaint,
|
||||
// dashWidth: 8.rpx,
|
||||
// dashSpace: 6.rpx,
|
||||
// );
|
||||
// }
|
||||
|
||||
// // 2. 绘制数据线段和圆点
|
||||
// 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);
|
||||
|
||||
// // 设置颜色(根据 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);
|
||||
// }
|
||||
|
||||
// // 3. Y轴辅助线和文字
|
||||
// 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,
|
||||
// );
|
||||
// }
|
||||
|
||||
// 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));
|
||||
// }
|
||||
|
||||
// // 4. X轴主线
|
||||
// canvas.drawLine(
|
||||
// Offset(xStart, chartHeight),
|
||||
// Offset(xEnd, chartHeight),
|
||||
// axisPaint,
|
||||
// );
|
||||
|
||||
// // 5. 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));
|
||||
|
||||
// // 6. 中间小时刻度
|
||||
// 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;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
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 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 (showLabel.isEmpty) return const SizedBox();
|
||||
if (widget.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(widget.startTime);
|
||||
DateTime maxTime = DateTime.fromMillisecondsSinceEpoch(widget.endTime);
|
||||
|
||||
DateTime minTime = DateTime.fromMillisecondsSinceEpoch(startTime);
|
||||
DateTime maxTime = DateTime.fromMillisecondsSinceEpoch(endTime);
|
||||
return GestureDetector(
|
||||
onTapDown: (details) {
|
||||
RenderBox box = context.findRenderObject() as RenderBox;
|
||||
final localPosition = box.globalToLocal(details.globalPosition);
|
||||
|
||||
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,
|
||||
// 查找是否点击到某个点
|
||||
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'
|
||||
'次数: ${selectedData!['times']}',
|
||||
style: TextStyle(
|
||||
fontSize: 18.rpx,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LineChartByRangePainter extends CustomPainter {
|
||||
final List<Map<String, dynamic>> data;
|
||||
final int yMax;
|
||||
final int maxY;
|
||||
final DateTime minTime;
|
||||
final DateTime maxTime;
|
||||
final int? threshold;
|
||||
final int ySegments;
|
||||
|
||||
_LineChartByRangePainter({
|
||||
required this.data,
|
||||
required this.yMax,
|
||||
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; // X轴标签缩进距离
|
||||
double labelInset = 12.rpx;
|
||||
|
||||
// 绘图X轴起止点,考虑内缩labelInset
|
||||
final double xStart = padding + labelInset;
|
||||
final double xEnd = size.width - padding - labelInset;
|
||||
final double chartWidth = xEnd - xStart;
|
||||
@@ -78,17 +526,28 @@ class _LineChartByRangePainter extends CustomPainter {
|
||||
maxTime.millisecondsSinceEpoch - minTime.millisecondsSinceEpoch;
|
||||
if (totalDuration <= 0) return;
|
||||
|
||||
Paint linePaint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 3.rpx
|
||||
..color = stringToColor("#00C1AA")
|
||||
..strokeCap = StrokeCap.round;
|
||||
Paint axisPaint = Paint()
|
||||
..color = Colors.grey.withOpacity(0.4)
|
||||
..strokeWidth = 1.rpx;
|
||||
|
||||
Paint fillCirclePaint = Paint()
|
||||
..style = PaintingStyle.fill
|
||||
..color = stringToColor("#00C1AA");
|
||||
Paint thresholdPaint = Paint()
|
||||
..color = themeController.currentColor.sc9
|
||||
..strokeWidth = 1.rpx;
|
||||
|
||||
// 1. 先绘制数据线段及起止点圆点
|
||||
// 阈值虚线(红色)
|
||||
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'];
|
||||
@@ -98,31 +557,41 @@ class _LineChartByRangePainter extends CustomPainter {
|
||||
chartWidth * (start - minTime.millisecondsSinceEpoch) / totalDuration;
|
||||
double endX = xStart +
|
||||
chartWidth * (end - minTime.millisecondsSinceEpoch) / totalDuration;
|
||||
double y = chartHeight * (1 - times / yMax);
|
||||
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), linePaint);
|
||||
canvas.drawLine(Offset(startX, y), Offset(endX, y), dynamicLinePaint);
|
||||
|
||||
// 画起点圆点
|
||||
canvas.drawCircle(Offset(startX, y), 4.rpx, fillCirclePaint);
|
||||
|
||||
// 画终点圆点
|
||||
canvas.drawCircle(Offset(endX, y), 4.rpx, fillCirclePaint);
|
||||
// 画起点和终点圆点
|
||||
canvas.drawCircle(Offset(startX, y), 6.rpx, dynamicCirclePaint);
|
||||
canvas.drawCircle(Offset(endX, y), 6.rpx, dynamicCirclePaint);
|
||||
}
|
||||
|
||||
// 2. Y轴辅助线及文字
|
||||
Paint axisPaint = Paint()
|
||||
..color = Colors.grey.withOpacity(0.4)
|
||||
..strokeWidth = 1.rpx;
|
||||
// Y轴辅助线和文字
|
||||
for (int i = 0; i <= ySegments; i++) {
|
||||
double y = chartHeight * i / ySegments;
|
||||
|
||||
for (int i = 0; i <= 6; i++) {
|
||||
double y = chartHeight * i / 6;
|
||||
|
||||
if (i == 6) {
|
||||
// 实线
|
||||
if (i == ySegments) {
|
||||
canvas.drawLine(Offset(xStart, y), Offset(xEnd, y), axisPaint);
|
||||
} else {
|
||||
// 虚线
|
||||
drawDashedLine(
|
||||
canvas,
|
||||
Offset(xStart, y),
|
||||
@@ -133,12 +602,13 @@ class _LineChartByRangePainter extends CustomPainter {
|
||||
);
|
||||
}
|
||||
|
||||
// Y轴文字
|
||||
TextPainter tp = TextPainter(
|
||||
text: TextSpan(
|
||||
text: '${yMax - (yMax * i / 6).round()}',
|
||||
text: '${maxY - (maxY * i / ySegments).round()}',
|
||||
style: TextStyle(
|
||||
fontSize: 18.rpx, color: themeController.currentColor.sc4),
|
||||
fontSize: 18.rpx,
|
||||
color: themeController.currentColor.sc4,
|
||||
),
|
||||
),
|
||||
textDirection: ui.TextDirection.ltr,
|
||||
);
|
||||
@@ -146,29 +616,14 @@ class _LineChartByRangePainter extends CustomPainter {
|
||||
tp.paint(canvas, Offset(0, y - tp.height / 2));
|
||||
}
|
||||
|
||||
// 3. X轴线
|
||||
// X轴主线
|
||||
canvas.drawLine(
|
||||
Offset(xStart, chartHeight), Offset(xEnd, chartHeight), axisPaint);
|
||||
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
|
||||
// X轴时间文字(左右两侧)
|
||||
String leftLabel = DateFormat('HH:mm').format(minTime);
|
||||
TextPainter leftTp = TextPainter(
|
||||
text: TextSpan(
|
||||
@@ -184,7 +639,6 @@ class _LineChartByRangePainter extends CustomPainter {
|
||||
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(
|
||||
@@ -202,7 +656,10 @@ class _LineChartByRangePainter extends CustomPainter {
|
||||
Offset(size.width - padding - labelInset - rightTp.width / 2,
|
||||
chartHeight + 8.rpx));
|
||||
|
||||
// 7. 中间小时数字(23, 0, 1, 2, ...)
|
||||
// 中间小时刻度
|
||||
int totalHours = maxTime.difference(minTime).inHours + 1;
|
||||
int startHour = minTime.hour;
|
||||
|
||||
for (int i = 1; i < totalHours; i++) {
|
||||
double x = xStart + chartWidth * i / totalHours;
|
||||
|
||||
@@ -220,7 +677,6 @@ class _LineChartByRangePainter extends CustomPainter {
|
||||
textDirection: ui.TextDirection.ltr,
|
||||
);
|
||||
tp.layout();
|
||||
|
||||
tp.paint(canvas, Offset(x - tp.width / 2, chartHeight + 8.rpx));
|
||||
}
|
||||
}
|
||||
|
||||
199
lib/pages/sleep_report/chart/SnoreChart.dart
Normal file
199
lib/pages/sleep_report/chart/SnoreChart.dart
Normal file
@@ -0,0 +1,199 @@
|
||||
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 StatelessWidget {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(
|
||||
size: Size(double.infinity, 500.rpx),
|
||||
painter: BarChartPainter(
|
||||
data,
|
||||
startTime,
|
||||
endTime,
|
||||
maxYValue: maxYValue,
|
||||
yStepCount: yStepCount,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BarChartPainter extends CustomPainter {
|
||||
final List<BarData> data;
|
||||
final int startTime;
|
||||
final int endTime;
|
||||
final double maxYValue;
|
||||
final int yStepCount;
|
||||
|
||||
final double topPadding = 0; // 控制顶部间距
|
||||
final double bottomPadding = 0; // 控制底部间距
|
||||
final double leftPadding = 30.rpx;
|
||||
// final double labelHeight = 50.rpx;
|
||||
|
||||
BarChartPainter(
|
||||
this.data,
|
||||
this.startTime,
|
||||
this.endTime, {
|
||||
required this.maxYValue,
|
||||
this.yStepCount = 5,
|
||||
});
|
||||
|
||||
@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;
|
||||
|
||||
// 横线
|
||||
// canvas.drawLine(
|
||||
// Offset(leftPadding, y),
|
||||
// Offset(size.width, y),
|
||||
// Paint()
|
||||
// ..color = Colors.grey.withOpacity(0.3)
|
||||
// ..strokeWidth = 0.5,
|
||||
// );
|
||||
final dashPaint = Paint()
|
||||
..color = Colors.grey.withOpacity(0.5)
|
||||
..strokeWidth = 0.5;
|
||||
|
||||
drawDashedLine(
|
||||
canvas, Offset(leftPadding, y), Offset(size.width, y), dashPaint);
|
||||
|
||||
// Y轴刻度文字
|
||||
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 xPaint = Paint()..color = Colors.grey;
|
||||
|
||||
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));
|
||||
|
||||
// 绘制柱子
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,6 +164,114 @@ class SnoreWaveform extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
// ..strokeWidth = 1.5
|
||||
// ..style = PaintingStyle.stroke;
|
||||
|
||||
// final Path upperPath = Path();
|
||||
// final Path lowerPath = Path();
|
||||
// const double scaleY = 0.5; //波形图比例
|
||||
|
||||
// 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
|
||||
// ..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;
|
||||
// if (t == startTime) {
|
||||
// label = DateFormat('HH:mm').format(dt); // 起点显示 HH:mm
|
||||
// } else {
|
||||
// label = DateFormat('h').format(dt); // 中间显示小时,不带前导0
|
||||
// }
|
||||
|
||||
// textPainter.text = TextSpan(
|
||||
// text: label,
|
||||
// style: TextStyle(fontSize: 10, color: Colors.grey),
|
||||
// );
|
||||
// textPainter.layout();
|
||||
// textPainter.paint(
|
||||
// canvas,
|
||||
// Offset(x - textPainter.width / 2, height + 20.rpx),
|
||||
// );
|
||||
// }
|
||||
|
||||
// // 单独绘制终点时间标签,确保显示具体时分
|
||||
// {
|
||||
// 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 + 20.rpx),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// @override
|
||||
// bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
// }
|
||||
|
||||
class SnoreWaveformPainter extends CustomPainter {
|
||||
final List<dynamic> snoreValues;
|
||||
final int startTime;
|
||||
@@ -184,13 +292,22 @@ class SnoreWaveformPainter extends CustomPainter {
|
||||
final double pixelPerMs = width / totalDuration;
|
||||
|
||||
final Paint wavePaint = Paint()
|
||||
..color = stringToColor("#8E7DEF")
|
||||
..color = stringToColor("#8E7DEF").withOpacity(0.8)
|
||||
..strokeWidth = 1.5
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
final Path upperPath = Path();
|
||||
final Path lowerPath = Path();
|
||||
const double scaleY = 0.5; //波形图比例
|
||||
|
||||
// ✅ 获取最大值用于自适应比例
|
||||
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"];
|
||||
@@ -212,32 +329,26 @@ class SnoreWaveformPainter extends CustomPainter {
|
||||
canvas.drawPath(upperPath, wavePaint);
|
||||
canvas.drawPath(lowerPath, wavePaint);
|
||||
|
||||
// ✅ 最后绘制中心线,防止被覆盖
|
||||
final Paint axisPaint = Paint()
|
||||
..color = Colors.grey
|
||||
..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;
|
||||
if (t == startTime) {
|
||||
label = DateFormat('HH:mm').format(dt); // 起点显示 HH:mm
|
||||
} else {
|
||||
label = DateFormat('h').format(dt); // 中间显示小时,不带前导0
|
||||
}
|
||||
String label = t == startTime
|
||||
? DateFormat('HH:mm').format(dt)
|
||||
: DateFormat('h').format(dt); // 12小时制
|
||||
|
||||
textPainter.text = TextSpan(
|
||||
text: label,
|
||||
@@ -246,11 +357,11 @@ class SnoreWaveformPainter extends CustomPainter {
|
||||
textPainter.layout();
|
||||
textPainter.paint(
|
||||
canvas,
|
||||
Offset(x - textPainter.width / 2, height + 20.rpx),
|
||||
Offset(x - textPainter.width / 2, height + 2), // 标签显示在底部
|
||||
);
|
||||
}
|
||||
|
||||
// 单独绘制终点时间标签,确保显示具体时分
|
||||
// ✅ 画终点时间
|
||||
{
|
||||
double x = (endTime - startTime) * pixelPerMs;
|
||||
DateTime dt = DateTime.fromMillisecondsSinceEpoch(endTime);
|
||||
@@ -263,7 +374,7 @@ class SnoreWaveformPainter extends CustomPainter {
|
||||
textPainter.layout();
|
||||
textPainter.paint(
|
||||
canvas,
|
||||
Offset(x - textPainter.width / 2, height + 20.rpx),
|
||||
Offset(x - textPainter.width / 2, height + 2),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user