更新
This commit is contained in:
119
lib/pages/sleep_report/chart/AdviceComponnetWidget.dart
Normal file
119
lib/pages/sleep_report/chart/AdviceComponnetWidget.dart
Normal file
@@ -0,0 +1,119 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.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 AdviceComponnetWidget extends StatefulWidget {
|
||||
final String title; // 建议标题
|
||||
final String description; // 建议说明
|
||||
|
||||
const AdviceComponnetWidget({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.description,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AdviceComponnetWidget> createState() => _AdviceComponnetWidgetState();
|
||||
}
|
||||
|
||||
class _AdviceComponnetWidgetState extends State<AdviceComponnetWidget> {
|
||||
@override
|
||||
void setState(VoidCallback callback) {
|
||||
super.setState(callback);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
// 显示标题
|
||||
Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: stringToColor("#313541"),
|
||||
borderRadius: BorderRadius.circular(20.rpx),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
28.rpx, 30.rpx, 28.rpx, 30.rpx),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(),
|
||||
child: Text(
|
||||
widget.title, // 使用传入的标题
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 显示描述
|
||||
Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(),
|
||||
child: Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
28.rpx, 30.rpx, 28.rpx, 30.rpx),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize: 26.rpx, // 设置文字大小
|
||||
height: 1.3, // 设置行高,控制文字上下间距
|
||||
),
|
||||
children: [
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle, // 图标和文字垂直居中
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: 4.rpx, // 调整底部间距
|
||||
right: 10.rpx, // 适当调整图标右边距
|
||||
top: 4.rpx, // 增加顶部间距
|
||||
),
|
||||
child: SvgPicture.asset(
|
||||
'assets/img/icon/ai.svg', // 替换为你的 SVG 文件路径
|
||||
width: 37.rpx, // 设置适中的 SVG 图标大小
|
||||
height: 31.rpx, // 使图标和文字大小一致
|
||||
color:
|
||||
themeController.currentColor.sc2, // 设置 SVG 颜色
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: widget.description, // 使用传入的描述
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize: AppConstants().normal_text_fontSize, // 文字大小
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
108
lib/pages/sleep_report/chart/DataShowWidget.dart
Normal file
108
lib/pages/sleep_report/chart/DataShowWidget.dart
Normal file
@@ -0,0 +1,108 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
|
||||
class DataShowWidget extends StatefulWidget {
|
||||
final Widget widget1; // 第一个传入的 widget
|
||||
final Widget widget2; // 第二个传入的 widget
|
||||
final Widget widget3; // 第三个传入的 widget
|
||||
final Widget widget4; // 第四个传入的 widget
|
||||
final MainAxisAlignment alignment; // 控制 Row 的对齐方式
|
||||
|
||||
const DataShowWidget({
|
||||
super.key,
|
||||
required this.widget1,
|
||||
required this.widget2,
|
||||
required this.widget3,
|
||||
required this.widget4,
|
||||
this.alignment = MainAxisAlignment.start, // 默认左对齐
|
||||
});
|
||||
|
||||
@override
|
||||
State<DataShowWidget> createState() => _DataShowWidgetState();
|
||||
}
|
||||
|
||||
class _DataShowWidgetState extends State<DataShowWidget> {
|
||||
@override
|
||||
void setState(VoidCallback callback) {
|
||||
super.setState(callback);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 66.rpx,
|
||||
decoration: BoxDecoration(),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal, // 设置横向滚动
|
||||
child: Row(
|
||||
mainAxisAlignment: widget.alignment, // 根据传入的 alignment 控制对齐方式
|
||||
children: [
|
||||
// 放入传入的 widget1
|
||||
Container(
|
||||
width: MediaQuery.sizeOf(context).width * 0.4, // 固定宽度
|
||||
decoration: BoxDecoration(),
|
||||
child: Align(
|
||||
alignment: widget.alignment == MainAxisAlignment.start
|
||||
? Alignment.centerLeft
|
||||
: widget.alignment == MainAxisAlignment.center
|
||||
? Alignment.center
|
||||
: Alignment.centerRight, // 根据传入的 alignment 设置对齐
|
||||
child: widget.widget1, // 显示传入的 widget1
|
||||
),
|
||||
),
|
||||
// 放入传入的 widget2
|
||||
Container(
|
||||
width: MediaQuery.sizeOf(context).width * 0.15, // 固定宽度
|
||||
decoration: BoxDecoration(),
|
||||
child: Align(
|
||||
alignment: widget.alignment == MainAxisAlignment.start
|
||||
? Alignment.centerLeft
|
||||
: widget.alignment == MainAxisAlignment.center
|
||||
? Alignment.center
|
||||
: Alignment.centerRight, // 同样设置对齐
|
||||
child: widget.widget2, // 显示传入的 widget2
|
||||
),
|
||||
),
|
||||
// 放入传入的 widget3
|
||||
Container(
|
||||
width: MediaQuery.sizeOf(context).width * 0.2, // 固定宽度
|
||||
decoration: BoxDecoration(),
|
||||
child: Align(
|
||||
alignment: widget.alignment == MainAxisAlignment.start
|
||||
? Alignment.centerLeft
|
||||
: widget.alignment == MainAxisAlignment.center
|
||||
? Alignment.center
|
||||
: Alignment.centerRight, // 同样设置对齐
|
||||
child: widget.widget3, // 显示传入的 widget3
|
||||
),
|
||||
),
|
||||
// 放入传入的 widget4
|
||||
Container(
|
||||
width: MediaQuery.sizeOf(context).width * 0.15, // 固定宽度
|
||||
decoration: BoxDecoration(),
|
||||
child: Align(
|
||||
alignment: widget.alignment == MainAxisAlignment.start
|
||||
? Alignment.centerLeft
|
||||
: widget.alignment == MainAxisAlignment.center
|
||||
? Alignment.center
|
||||
: Alignment.centerRight, // 同样设置对齐
|
||||
child: widget.widget4, // 显示传入的 widget4
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -204,7 +204,7 @@ class SingleBarPainter extends CustomPainter {
|
||||
TextStyle(color: themeController.currentColor.sc3, fontSize: 26.rpx);
|
||||
final textPainter = TextPainter(textDirection: TextDirection.ltr);
|
||||
textPainter.text =
|
||||
TextSpan(text: '${value.toStringAsFixed(0)}%', style: textStyle);
|
||||
TextSpan(text: '${value.toStringAsFixed(0)}', style: textStyle);
|
||||
textPainter.layout();
|
||||
canvas.save();
|
||||
canvas.clipRect(Rect.fromLTWH(
|
||||
|
||||
158
lib/pages/sleep_report/chart/LineChart.dart
Normal file
158
lib/pages/sleep_report/chart/LineChart.dart
Normal file
@@ -0,0 +1,158 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
class LineChart extends StatelessWidget {
|
||||
final int startTime;
|
||||
final int endTime;
|
||||
final double minValue;
|
||||
final double maxValue;
|
||||
final List<Map<String, dynamic>> dataPoints;
|
||||
|
||||
LineChart({
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.minValue,
|
||||
required this.maxValue,
|
||||
required this.dataPoints,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(
|
||||
size: Size(double.infinity, 300),
|
||||
painter: LineChartPainter(
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
minValue: minValue,
|
||||
maxValue: maxValue,
|
||||
dataPoints: dataPoints,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LineChartPainter extends CustomPainter {
|
||||
final int startTime;
|
||||
final int endTime;
|
||||
final double minValue;
|
||||
final double maxValue;
|
||||
final List<Map<String, dynamic>> dataPoints;
|
||||
|
||||
LineChartPainter({
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.minValue,
|
||||
required this.maxValue,
|
||||
required this.dataPoints,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
Paint paint = Paint()..style = PaintingStyle.stroke;
|
||||
double chartWidth = size.width;
|
||||
double chartHeight = size.height;
|
||||
|
||||
// 时间轴刻度设置
|
||||
DateFormat timeFormatStartEnd = DateFormat('HH:mm');
|
||||
DateFormat timeFormatMiddle = DateFormat('h');
|
||||
|
||||
// 绘制Y轴刻度
|
||||
double yAxisHeight = chartHeight - 40; // 留一些空间给X轴
|
||||
double yAxisStep = yAxisHeight / 4;
|
||||
paint.color = Colors.grey;
|
||||
paint.strokeWidth = 1;
|
||||
canvas.drawLine(Offset(30, 0), Offset(30, chartHeight), paint); // Y轴
|
||||
|
||||
// 绘制Y轴的刻度线
|
||||
paint.color = Colors.grey;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
double y = i * yAxisStep;
|
||||
if (i == 0) {
|
||||
paint.color = Colors.grey; // 0线
|
||||
canvas.drawLine(Offset(25, y), Offset(35, y), paint);
|
||||
} else if (i == 1) {
|
||||
paint.color = Colors.red; // 最小值线
|
||||
paint.style = PaintingStyle.stroke;
|
||||
paint.strokeWidth = 1;
|
||||
canvas.drawLine(Offset(25, y), Offset(35, y), paint);
|
||||
} else if (i == 2) {
|
||||
paint.color = Colors.grey; // 最大值与最小值中间线
|
||||
paint.style = PaintingStyle.stroke;
|
||||
paint.strokeWidth = 1;
|
||||
_drawDashedLine(canvas, paint, 25, y, 35, y); // Custom dashed line
|
||||
} else {
|
||||
paint.color = Colors.red; // 最大值线
|
||||
canvas.drawLine(Offset(25, y), Offset(35, y), paint);
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制X轴时间刻度
|
||||
DateTime startDate = DateTime.fromMillisecondsSinceEpoch(startTime);
|
||||
DateTime endDate = DateTime.fromMillisecondsSinceEpoch(endTime);
|
||||
double xAxisStep = (chartWidth - 60) /
|
||||
(endDate.millisecondsSinceEpoch - startDate.millisecondsSinceEpoch);
|
||||
|
||||
for (DateTime date = startDate;
|
||||
date.isBefore(endDate);
|
||||
date = date.add(Duration(hours: 1))) {
|
||||
String timeLabel = (date == startDate || date == endDate)
|
||||
? timeFormatStartEnd.format(date)
|
||||
: timeFormatMiddle.format(date);
|
||||
paint.color = Colors.black;
|
||||
|
||||
// Draw text using TextPainter
|
||||
_drawText(
|
||||
canvas, timeLabel, Offset(30, yAxisHeight)); // Position dynamically
|
||||
}
|
||||
|
||||
// 绘制折线图数据点
|
||||
Path path = Path();
|
||||
for (var i = 0; i < dataPoints.length; i++) {
|
||||
var point = dataPoints[i];
|
||||
DateTime pointTime = DateTime.fromMillisecondsSinceEpoch(point['time']);
|
||||
double x = (pointTime.millisecondsSinceEpoch -
|
||||
startDate.millisecondsSinceEpoch) *
|
||||
xAxisStep +
|
||||
30;
|
||||
double y = chartHeight -
|
||||
(point['value'] - minValue) * yAxisHeight / (maxValue - minValue);
|
||||
path.lineTo(x, y);
|
||||
}
|
||||
|
||||
paint.color = Colors.green; // Line color based on range
|
||||
paint.strokeWidth = 2;
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
// Custom method to draw dashed line
|
||||
void _drawDashedLine(Canvas canvas, Paint paint, double startX, double startY,
|
||||
double endX, double endY) {
|
||||
double dashWidth = 5;
|
||||
double dashSpace = 3;
|
||||
double distance = (endX - startX).abs();
|
||||
double dashCount = (distance / (dashWidth + dashSpace)).floorToDouble();
|
||||
for (int i = 0; i < dashCount; i++) {
|
||||
double startXDash = startX + (i * (dashWidth + dashSpace));
|
||||
double endXDash = startXDash + dashWidth;
|
||||
canvas.drawLine(
|
||||
Offset(startXDash, startY), Offset(endXDash, endY), paint);
|
||||
}
|
||||
}
|
||||
|
||||
// Custom method to draw text
|
||||
void _drawText(Canvas canvas, String text, Offset offset) {
|
||||
TextPainter textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: text, style: TextStyle(color: Colors.black, fontSize: 12)),
|
||||
textDirection: ui.TextDirection.ltr,
|
||||
)..layout();
|
||||
|
||||
textPainter.paint(canvas, offset);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -154,19 +154,19 @@ class _LineChartByRangePainter extends CustomPainter {
|
||||
int totalHours = maxTime.difference(minTime).inHours;
|
||||
int startHour = minTime.hour;
|
||||
|
||||
for (int i = 1; i < totalHours; i++) {
|
||||
double x = xStart + chartWidth * i / totalHours;
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
// // 垂直虚线
|
||||
// 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);
|
||||
|
||||
109
lib/pages/sleep_report/chart/RadarChart.dart
Normal file
109
lib/pages/sleep_report/chart/RadarChart.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:ui' as ui;
|
||||
import 'dart:math';
|
||||
|
||||
class RadarChart extends StatelessWidget {
|
||||
final List<List<double>> data; // 存储多个数据集
|
||||
final List<String> labels; // 每个角的标签
|
||||
final double maxValue; // 数据的最大值,用来统一尺度
|
||||
|
||||
const RadarChart({
|
||||
Key? key,
|
||||
required this.data,
|
||||
required this.labels,
|
||||
this.maxValue = 100,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(
|
||||
size: Size(300, 300), // 图表的大小
|
||||
painter: RadarChartPainter(
|
||||
data: data,
|
||||
labels: labels,
|
||||
maxValue: maxValue,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RadarChartPainter extends CustomPainter {
|
||||
final List<List<double>> data;
|
||||
final List<String> labels;
|
||||
final double maxValue;
|
||||
|
||||
RadarChartPainter({
|
||||
required this.data,
|
||||
required this.labels,
|
||||
required this.maxValue,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
Paint paintLine = Paint()
|
||||
..color = Colors.blue
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2;
|
||||
|
||||
Paint axisPaint = Paint()
|
||||
..color = Colors.grey.withOpacity(0.5)
|
||||
..strokeWidth = 1;
|
||||
|
||||
double centerX = size.width / 2;
|
||||
double centerY = size.height / 2;
|
||||
double radius = size.width / 2;
|
||||
|
||||
int numOfPoints = labels.length;
|
||||
|
||||
// 绘制雷达图的轴线
|
||||
for (int i = 0; i < numOfPoints; i++) {
|
||||
double angle = (2 * pi / numOfPoints) * i;
|
||||
double x = centerX + radius * cos(angle);
|
||||
double y = centerY + radius * sin(angle);
|
||||
|
||||
// 画轴线
|
||||
canvas.drawLine(Offset(centerX, centerY), Offset(x, y), axisPaint);
|
||||
|
||||
// 绘制标签
|
||||
TextPainter tp = TextPainter(
|
||||
text: TextSpan(
|
||||
text: labels[i],
|
||||
style: TextStyle(color: Colors.black, fontSize: 12),
|
||||
),
|
||||
textDirection: ui.TextDirection.ltr,
|
||||
);
|
||||
tp.layout();
|
||||
tp.paint(canvas, Offset(x + 8, y - 8)); // 设置标签位置
|
||||
}
|
||||
|
||||
// 绘制多个数据集
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
Paint fillPaint = Paint()
|
||||
..color = Colors.primaries[i % Colors.primaries.length].withOpacity(0.3)
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
Paint linePaint = Paint()
|
||||
..color = Colors.primaries[i % Colors.primaries.length]
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2;
|
||||
|
||||
List<Offset> points = [];
|
||||
for (int j = 0; j < numOfPoints; j++) {
|
||||
double angle = (2 * pi / numOfPoints) * j;
|
||||
double pointRadius = (data[i][j] / maxValue) * radius;
|
||||
double x = centerX + pointRadius * cos(angle);
|
||||
double y = centerY + pointRadius * sin(angle);
|
||||
|
||||
points.add(Offset(x, y));
|
||||
}
|
||||
|
||||
// 画出数据连接线
|
||||
Path path = Path()..addPolygon(points, true);
|
||||
canvas.drawPath(path, linePaint);
|
||||
canvas.drawPath(path, fillPaint);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
}
|
||||
118
lib/pages/sleep_report/chart/ScatterPlotChart.dart
Normal file
118
lib/pages/sleep_report/chart/ScatterPlotChart.dart
Normal file
@@ -0,0 +1,118 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
|
||||
class ScatterPlotChart extends StatelessWidget {
|
||||
final List<ScatterSpot> points;
|
||||
final int xMax;
|
||||
final int yMax;
|
||||
final Color pointColor;
|
||||
final int divisions;
|
||||
|
||||
ScatterPlotChart({
|
||||
required this.points,
|
||||
required this.xMax,
|
||||
required this.yMax,
|
||||
required this.pointColor,
|
||||
required this.divisions,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 计算向上取整后的最大值
|
||||
double xMaxCeil = (xMax / 100).ceil() * 100.0;
|
||||
double yMaxCeil = (yMax / 100).ceil() * 100.0;
|
||||
|
||||
return SizedBox(
|
||||
child: ScatterChart(
|
||||
ScatterChartData(
|
||||
backgroundColor: Colors.transparent,
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
horizontalInterval: yMaxCeil / divisions,
|
||||
verticalInterval: xMaxCeil / divisions,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return FlLine(
|
||||
color: themeController.currentColor.sc4, // 设置网格线颜色
|
||||
strokeWidth: 0.5,
|
||||
);
|
||||
},
|
||||
getDrawingVerticalLine: (value) {
|
||||
return FlLine(
|
||||
color: themeController.currentColor.sc4, // 设置网格线颜色
|
||||
strokeWidth: 0.5,
|
||||
);
|
||||
},
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 60.rpx, // 给 y 轴标签更多空间
|
||||
getTitlesWidget: (double value, TitleMeta meta) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(right: 14.rpx), // 右侧加间距
|
||||
child: Text(
|
||||
value.toStringAsFixed(0),
|
||||
style: TextStyle(
|
||||
fontSize: 18.rpx,
|
||||
color: themeController.currentColor.sc4,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.right, // 右对齐
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (double value, TitleMeta meta) {
|
||||
return Text(
|
||||
value.toStringAsFixed(0),
|
||||
style: TextStyle(
|
||||
fontSize: 18.rpx,
|
||||
color: themeController.currentColor.sc4,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
rightTitles: AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
topTitles: AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(
|
||||
show: true,
|
||||
border: Border.all(
|
||||
color: themeController.currentColor.sc4,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
// 修改散点的大小和颜色
|
||||
scatterSpots: points.map((point) {
|
||||
return ScatterSpot(
|
||||
point.x, // x 坐标
|
||||
point.y, // y 坐标
|
||||
dotPainter: FlDotCirclePainter(
|
||||
radius: 3.rpx, // 自定义大小
|
||||
color: pointColor, // 自定义颜色
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
minX: 0,
|
||||
maxX: xMaxCeil,
|
||||
minY: 0,
|
||||
maxY: yMaxCeil,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,7 @@ class SegmentedCircleWithCenterWidget extends StatelessWidget {
|
||||
Positioned(
|
||||
right: 60.rpx, // 放置在右侧
|
||||
child: SvgPicture.asset(
|
||||
'assets/img/icon/add.svg',
|
||||
'assets/img/icon/score_down.svg',
|
||||
width: 14.rpx,
|
||||
height: 22.rpx,
|
||||
color: themeController.currentColor.sc9,
|
||||
|
||||
100
lib/pages/sleep_report/chart/SleepRadarChart.dart
Normal file
100
lib/pages/sleep_report/chart/SleepRadarChart.dart
Normal file
@@ -0,0 +1,100 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:vbvs_app/common/color/appConstants.dart';
|
||||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
|
||||
class SleepRadarChart extends StatelessWidget {
|
||||
final Map<String, double> today;
|
||||
final Map<String, double> yesterday;
|
||||
|
||||
const SleepRadarChart({
|
||||
Key? key,
|
||||
required this.today,
|
||||
required this.yesterday,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
// 雷达图
|
||||
_buildRadarChart(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRadarChart() {
|
||||
return AspectRatio(
|
||||
aspectRatio: 1.3,
|
||||
child: RadarChart(
|
||||
RadarChartData(
|
||||
dataSets: [
|
||||
// 今日数据
|
||||
RadarDataSet(
|
||||
dataEntries: [
|
||||
RadarEntry(value: today['type1']!), // 呼吸暂停
|
||||
RadarEntry(value: today['type2']!), // 入睡时间
|
||||
RadarEntry(value: today['type3']!), // 离床次数
|
||||
RadarEntry(value: today['type4']!), // 深睡比例
|
||||
RadarEntry(value: today['type5']!), // 睡眠时长
|
||||
],
|
||||
borderColor: stringToColor("#00C1AA"),
|
||||
borderWidth: 2,
|
||||
fillColor: Colors.transparent,
|
||||
entryRadius: 0,
|
||||
),
|
||||
// 昨日数据
|
||||
RadarDataSet(
|
||||
dataEntries: [
|
||||
RadarEntry(value: yesterday['type1']!), // 呼吸暂停
|
||||
RadarEntry(value: yesterday['type2']!), // 入睡时间
|
||||
RadarEntry(value: yesterday['type3']!), // 离床次数
|
||||
RadarEntry(value: yesterday['type4']!), // 深睡比例
|
||||
RadarEntry(value: yesterday['type5']!), // 睡眠时长
|
||||
],
|
||||
borderColor: stringToColor("#FFD251"),
|
||||
borderWidth: 2,
|
||||
fillColor: Colors.transparent,
|
||||
entryRadius: 0,
|
||||
),
|
||||
],
|
||||
radarBackgroundColor: stringToColor("#343844"),
|
||||
radarBorderData:
|
||||
BorderSide(color: themeController.currentColor.sc4, width: 1),
|
||||
radarShape: RadarShape.polygon,
|
||||
titlePositionPercentageOffset: 0.2,
|
||||
titleTextStyle: TextStyle(
|
||||
fontSize: AppConstants().normal_text_fontSize,
|
||||
color: themeController.currentColor.sc3),
|
||||
getTitle: (index, angle) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return RadarChartTitle(text: '呼吸暂停');
|
||||
case 1:
|
||||
return RadarChartTitle(text: '入睡时间');
|
||||
case 2:
|
||||
return RadarChartTitle(text: '离床次数');
|
||||
case 3:
|
||||
return RadarChartTitle(text: '深睡比例');
|
||||
case 4:
|
||||
return RadarChartTitle(text: '睡眠时长');
|
||||
default:
|
||||
return const RadarChartTitle(text: '');
|
||||
}
|
||||
},
|
||||
tickCount: 5,
|
||||
ticksTextStyle:
|
||||
const TextStyle(color: Colors.transparent, fontSize: 10),
|
||||
// ticksColor: Colors.grey.shade300,
|
||||
gridBorderData: BorderSide(color: Colors.transparent, width: 1),
|
||||
tickBorderData:
|
||||
BorderSide(color: themeController.currentColor.sc4, width: 1),
|
||||
),
|
||||
swapAnimationDuration: const Duration(milliseconds: 400),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
273
lib/pages/sleep_report/chart/TimeLineChart.dart
Normal file
273
lib/pages/sleep_report/chart/TimeLineChart.dart
Normal file
@@ -0,0 +1,273 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
|
||||
class TimeLineChart extends StatelessWidget {
|
||||
final List<DataPoint> points;
|
||||
final double yMin;
|
||||
final double yMax;
|
||||
final int startTime;
|
||||
final int endTime;
|
||||
final double width;
|
||||
final double height;
|
||||
|
||||
const TimeLineChart({
|
||||
super.key,
|
||||
required this.points,
|
||||
required this.yMin,
|
||||
required this.yMax,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
this.width = 400,
|
||||
this.height = 300,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(
|
||||
size: Size(width, height),
|
||||
painter: _TimeLineChartPainter(
|
||||
points: points,
|
||||
yMin: yMin,
|
||||
yMax: yMax,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DataPoint {
|
||||
final int timestamp;
|
||||
final double value;
|
||||
|
||||
DataPoint(this.timestamp, this.value);
|
||||
}
|
||||
|
||||
class _TimeLineChartPainter extends CustomPainter {
|
||||
final List<DataPoint> points;
|
||||
final double yMin;
|
||||
final double yMax;
|
||||
final int startTime;
|
||||
final int endTime;
|
||||
|
||||
_TimeLineChartPainter({
|
||||
required this.points,
|
||||
required this.yMin,
|
||||
required this.yMax,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
_drawYAxis(canvas, size);
|
||||
_drawXAxis(canvas, size);
|
||||
_drawLine(canvas, size);
|
||||
}
|
||||
|
||||
void _drawXAxis(Canvas canvas, Size size) {
|
||||
const margin = 40.0;
|
||||
final paint = Paint()..color = Colors.black;
|
||||
final textStyle = const TextStyle(color: Colors.black, fontSize: 12);
|
||||
|
||||
// Draw X axis line
|
||||
canvas.drawLine(
|
||||
Offset(margin, size.height - margin),
|
||||
Offset(size.width - margin, size.height - margin),
|
||||
paint,
|
||||
);
|
||||
|
||||
// Generate time ticks
|
||||
final timeFormatStartEnd = DateFormat('HH:mm');
|
||||
final timeFormatMiddle = DateFormat('h');
|
||||
final startDateTime = DateTime.fromMillisecondsSinceEpoch(startTime);
|
||||
final endDateTime = DateTime.fromMillisecondsSinceEpoch(endTime);
|
||||
|
||||
List<DateTime> hourTicks = [];
|
||||
DateTime current = DateTime(
|
||||
startDateTime.year,
|
||||
startDateTime.month,
|
||||
startDateTime.day,
|
||||
startDateTime.hour,
|
||||
).add(const Duration(hours: 1));
|
||||
|
||||
while (current.isBefore(endDateTime)) {
|
||||
if (current.isAfter(startDateTime)) {
|
||||
hourTicks.add(current);
|
||||
}
|
||||
current = current.add(const Duration(hours: 1));
|
||||
}
|
||||
|
||||
void drawTick(DateTime time, bool isEdge) {
|
||||
final x = margin +
|
||||
((time.millisecondsSinceEpoch - startTime) / (endTime - startTime)) *
|
||||
(size.width - 2 * margin);
|
||||
|
||||
final text = isEdge
|
||||
? timeFormatStartEnd.format(time)
|
||||
: timeFormatMiddle.format(time);
|
||||
|
||||
_drawText(
|
||||
canvas,
|
||||
text,
|
||||
Offset(x, size.height - margin + 20),
|
||||
TextAlign.center,
|
||||
);
|
||||
}
|
||||
|
||||
drawTick(startDateTime, true);
|
||||
drawTick(endDateTime, true);
|
||||
for (var tick in hourTicks) {
|
||||
drawTick(tick, false);
|
||||
}
|
||||
}
|
||||
|
||||
void _drawYAxis(Canvas canvas, Size size) {
|
||||
const margin = 40.0;
|
||||
final midValue = (yMax + yMin) / 2;
|
||||
|
||||
// 计算三条虚线之间的垂直间距
|
||||
final lineSpacing = (size.height - 2 * margin) / 3; // 让三条线之间的间距相等
|
||||
|
||||
// 新增的 y=0 实线的垂直位置
|
||||
final zeroLinePosition = margin + lineSpacing * 3; // 确保 y=0 位于三条虚线下方
|
||||
|
||||
void drawLine(double value, Color color,
|
||||
{bool isDashed = false, bool isSolid = false}) {
|
||||
final y =
|
||||
(value - yMax) / (yMin - yMax) * (size.height - 2 * margin) + margin;
|
||||
|
||||
final path = Path();
|
||||
path.moveTo(margin, y);
|
||||
path.lineTo(size.width - margin, y);
|
||||
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..strokeWidth = (color != Colors.grey) ? 2 : 1
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
if (isDashed) {
|
||||
Path dashedPath = _createDashedPath(path, dashWidth: 5, dashSpace: 5);
|
||||
canvas.drawPath(dashedPath, paint);
|
||||
} else if (isSolid) {
|
||||
// 对于实线,直接绘制
|
||||
canvas.drawPath(path, paint);
|
||||
} else {
|
||||
// 默认使用虚线绘制
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制 y=0 的灰色实线,并将其放置在三条虚线的下方
|
||||
if (yMin < 0 && yMax > 0) {
|
||||
drawLine(0, Colors.grey, isSolid: true); // 灰色实线绘制 y=0 线
|
||||
}
|
||||
|
||||
// 绘制最小值、中间值、最大值的虚线
|
||||
drawLine(yMin, themeController.currentColor.sc9, isDashed: true);
|
||||
drawLine(midValue, themeController.currentColor.sc4, isDashed: true);
|
||||
drawLine(yMax, themeController.currentColor.sc9, isDashed: true);
|
||||
}
|
||||
|
||||
Path _createDashedPath(Path path,
|
||||
{required double dashWidth, required double dashSpace}) {
|
||||
final Path dashedPath = Path();
|
||||
final ui.PathMetrics metrics = path.computeMetrics();
|
||||
|
||||
for (ui.PathMetric metric in metrics) {
|
||||
double distance = 0;
|
||||
while (distance < metric.length) {
|
||||
dashedPath.addPath(
|
||||
metric.extractPath(distance, distance + dashWidth),
|
||||
Offset.zero,
|
||||
);
|
||||
distance += dashWidth + dashSpace;
|
||||
}
|
||||
}
|
||||
|
||||
return dashedPath;
|
||||
}
|
||||
|
||||
void _drawLine(Canvas canvas, Size size) {
|
||||
const margin = 40.0;
|
||||
final sortedPoints = points
|
||||
..sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||
|
||||
Path? currentPath;
|
||||
Paint currentPaint = _createPaint(Colors.green);
|
||||
|
||||
for (int i = 0; i < sortedPoints.length - 1; i++) {
|
||||
final p1 = sortedPoints[i];
|
||||
final p2 = sortedPoints[i + 1];
|
||||
|
||||
final x1 = margin +
|
||||
((p1.timestamp - startTime) / (endTime - startTime)) *
|
||||
(size.width - 2 * margin);
|
||||
final y1 = margin +
|
||||
(1 - (p1.value - yMin) / (yMax - yMin)) * (size.height - 2 * margin);
|
||||
|
||||
final x2 = margin +
|
||||
((p2.timestamp - startTime) / (endTime - startTime)) *
|
||||
(size.width - 2 * margin);
|
||||
final y2 = margin +
|
||||
(1 - (p2.value - yMin) / (yMax - yMin)) * (size.height - 2 * margin);
|
||||
|
||||
final shouldBeGreen = p1.value >= yMin &&
|
||||
p1.value <= yMax &&
|
||||
p2.value >= yMin &&
|
||||
p2.value <= yMax;
|
||||
|
||||
// 根据当前线段的状态来决定是否切换颜色和虚线状态
|
||||
if (shouldBeGreen != (currentPaint.color == Colors.green)) {
|
||||
if (currentPath != null) {
|
||||
canvas.drawPath(currentPath, currentPaint);
|
||||
}
|
||||
currentPath = Path();
|
||||
currentPaint = _createPaint(shouldBeGreen ? Colors.green : Colors.red);
|
||||
}
|
||||
|
||||
currentPath ??= Path();
|
||||
if (i == 0) currentPath.moveTo(x1, y1);
|
||||
currentPath.lineTo(x2, y2);
|
||||
}
|
||||
|
||||
// 绘制剩余路径
|
||||
if (currentPath != null) {
|
||||
if (currentPaint.color == Colors.red) {
|
||||
// 如果是红色线,绘制虚线
|
||||
final dashedPath =
|
||||
_createDashedPath(currentPath, dashWidth: 5, dashSpace: 5);
|
||||
canvas.drawPath(dashedPath, currentPaint);
|
||||
} else {
|
||||
canvas.drawPath(currentPath, currentPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Paint _createPaint(Color color) => Paint()
|
||||
..color = color
|
||||
..strokeWidth = 2
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
void _drawText(Canvas canvas, String text, Offset offset, TextAlign align) {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: text,
|
||||
style: const TextStyle(color: Colors.black, fontSize: 12),
|
||||
),
|
||||
textDirection: ui.TextDirection.ltr,
|
||||
)..layout();
|
||||
|
||||
final centeredOffset = offset.translate(
|
||||
-textPainter.width / 2,
|
||||
-textPainter.height / 2,
|
||||
);
|
||||
textPainter.paint(canvas, centeredOffset);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
}
|
||||
281
lib/pages/sleep_report/chart/TimeSeriesChart.dart
Normal file
281
lib/pages/sleep_report/chart/TimeSeriesChart.dart
Normal file
@@ -0,0 +1,281 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
|
||||
class TimeSeriesChart extends StatelessWidget {
|
||||
final int startTime;
|
||||
final int endTime;
|
||||
final double yMin;
|
||||
final double yMax;
|
||||
final List<TimeSeriesPoint> dataPoints;
|
||||
|
||||
TimeSeriesChart({
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.yMin,
|
||||
required this.yMax,
|
||||
required this.dataPoints,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final midValue = (yMax + yMin) / 2;
|
||||
final xLabels = _generateXLabels();
|
||||
|
||||
// Prepare spots and segments
|
||||
List<FlSpot> spots = [];
|
||||
List<Color> lineColors = [];
|
||||
|
||||
for (int i = 0; i < dataPoints.length; i++) {
|
||||
final point = dataPoints[i];
|
||||
final xValue = _convertTimeToXValue(point.timestamp);
|
||||
final yValue = point.value;
|
||||
|
||||
spots.add(FlSpot(xValue, yValue));
|
||||
if (yValue >= yMin && yValue <= yMax) {
|
||||
lineColors.add(Colors.green); // Color for points within range
|
||||
} else {
|
||||
lineColors.add(Colors.red); // Color for points outside range
|
||||
}
|
||||
}
|
||||
|
||||
return AspectRatio(
|
||||
aspectRatio: 2,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
lineTouchData: LineTouchData(
|
||||
touchTooltipData: LineTouchTooltipData(
|
||||
getTooltipItems: (List<LineBarSpot> touchedSpots) {
|
||||
return touchedSpots.map((spot) {
|
||||
final time = DateTime.fromMillisecondsSinceEpoch(
|
||||
_convertXValueToTime(spot.x),
|
||||
);
|
||||
return LineTooltipItem(
|
||||
'${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}\n${spot.y.toStringAsFixed(0)}',
|
||||
const TextStyle(color: Colors.black),
|
||||
);
|
||||
}).toList();
|
||||
},
|
||||
),
|
||||
),
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
if (value == 0) {
|
||||
return FlLine(
|
||||
color: themeController.currentColor.sc4,
|
||||
strokeWidth: 1,
|
||||
);
|
||||
} else if (value == yMin) {
|
||||
return FlLine(
|
||||
color: themeController.currentColor.sc9,
|
||||
strokeWidth: 1,
|
||||
dashArray: [5, 5],
|
||||
);
|
||||
} else if (value == yMax) {
|
||||
return FlLine(
|
||||
color: themeController.currentColor.sc9,
|
||||
strokeWidth: 1,
|
||||
dashArray: [5, 5],
|
||||
);
|
||||
} else if (value == midValue) {
|
||||
return FlLine(
|
||||
color: themeController.currentColor.sc4,
|
||||
strokeWidth: 1,
|
||||
dashArray: [5, 5],
|
||||
);
|
||||
}
|
||||
return FlLine(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
strokeWidth: 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
rightTitles: AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
topTitles: AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 30,
|
||||
getTitlesWidget: (value, meta) {
|
||||
final index = value.toInt();
|
||||
if (index >= 0 && index < xLabels.length) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
xLabels[index].label,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Text('');
|
||||
},
|
||||
),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, meta) {
|
||||
if (value == 0) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(right: 14.rpx),
|
||||
child: Text(
|
||||
value.toStringAsFixed(0),
|
||||
style: TextStyle(
|
||||
fontSize: 18.rpx,
|
||||
color: themeController.currentColor.sc4,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
);
|
||||
} else if (value == yMin) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(right: 14.rpx),
|
||||
child: Text(
|
||||
yMin.toStringAsFixed(0),
|
||||
style: TextStyle(
|
||||
fontSize: 18.rpx,
|
||||
color: themeController.currentColor.sc4,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
);
|
||||
} else if (value == midValue) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(right: 14.rpx),
|
||||
child: Text(
|
||||
midValue.toStringAsFixed(0),
|
||||
style: TextStyle(
|
||||
fontSize: 18.rpx,
|
||||
color: themeController.currentColor.sc4,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
);
|
||||
} else if (value == yMax) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(right: 14.rpx),
|
||||
child: Text(
|
||||
yMax.toStringAsFixed(0),
|
||||
style: TextStyle(
|
||||
fontSize: 18.rpx,
|
||||
color: themeController.currentColor.sc4,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Text('');
|
||||
},
|
||||
reservedSize: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(
|
||||
show: false,
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.3)),
|
||||
),
|
||||
minX: 0,
|
||||
maxX: xLabels.length - 1,
|
||||
minY: min(0, yMin) - (yMax - yMin) * 0.2,
|
||||
maxY: yMax + (yMax - yMin) * 0.2,
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: spots,
|
||||
isCurved: false,
|
||||
color: themeController.currentColor.sc2,
|
||||
barWidth: 2,
|
||||
isStrokeCapRound: true,
|
||||
dotData: FlDotData(show: false), // Disable dots
|
||||
belowBarData: BarAreaData(show: false),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<XLabel> _generateXLabels() {
|
||||
final labels = <XLabel>[];
|
||||
final startDate = DateTime.fromMillisecondsSinceEpoch(startTime);
|
||||
final endDate = DateTime.fromMillisecondsSinceEpoch(endTime);
|
||||
|
||||
labels.add(XLabel(
|
||||
time: startTime,
|
||||
label:
|
||||
'${startDate.hour.toString().padLeft(2, '0')}:${startDate.minute.toString().padLeft(2, '0')}',
|
||||
));
|
||||
|
||||
DateTime current = DateTime(
|
||||
startDate.year,
|
||||
startDate.month,
|
||||
startDate.day,
|
||||
startDate.hour + 1,
|
||||
);
|
||||
|
||||
while (current.isBefore(endDate)) {
|
||||
labels.add(XLabel(
|
||||
time: current.millisecondsSinceEpoch,
|
||||
label: current.hour.toString(),
|
||||
));
|
||||
current = current.add(Duration(hours: 1));
|
||||
}
|
||||
|
||||
labels.add(XLabel(
|
||||
time: endTime,
|
||||
label:
|
||||
'${endDate.hour.toString().padLeft(2, '0')}:${endDate.minute.toString().padLeft(2, '0')}',
|
||||
));
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
double _convertTimeToXValue(int timestamp) {
|
||||
final totalDuration = endTime - startTime;
|
||||
final pointDuration = timestamp - startTime;
|
||||
final xLabels = _generateXLabels();
|
||||
return (pointDuration / totalDuration) * (xLabels.length - 1);
|
||||
}
|
||||
|
||||
int _convertXValueToTime(double xValue) {
|
||||
final xLabels = _generateXLabels();
|
||||
final totalDuration = endTime - startTime;
|
||||
final ratio = xValue / (xLabels.length - 1);
|
||||
return startTime + (totalDuration * ratio).round();
|
||||
}
|
||||
}
|
||||
|
||||
class TimeSeriesPoint {
|
||||
final int timestamp;
|
||||
final double value;
|
||||
|
||||
TimeSeriesPoint(this.timestamp, this.value);
|
||||
}
|
||||
|
||||
class XLabel {
|
||||
final int time;
|
||||
final String label;
|
||||
|
||||
XLabel({required this.time, required this.label});
|
||||
}
|
||||
Reference in New Issue
Block a user