Files
tuiche/lib/pages/sleep_report/component/MonthDataWidget.dart
2025-07-21 16:50:57 +08:00

792 lines
29 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 'package:ef/base/chart/drawer.dart';
import 'package:flutter/material.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/pages/mh_page/component/easychart.dart';
import 'package:vbvs_app/pages/sleep_report/component/SleepCard.dart';
import 'package:vbvs_app/pages/sleep_report/component/SleepChartWidget.dart';
import 'package:vbvs_app/pages/sleep_report/component/TrendDataTablePage.dart';
import 'package:vbvs_app/pages/sleep_report/component/TrendDataTextPage.dart';
import 'package:vbvs_app/pages/sleep_report/component/WeekSleepScoreWidget.dart';
Widget MonthDataWidget(
Map<dynamic, dynamic> sleepReport,
dynamic data,
) {
List<Widget> _buildSectionList() {
EdgeInsetsDirectional padding =
EdgeInsetsDirectional.fromSTEB(30.rpx, 0, 30.rpx, 25.rpx);
return [
AvgSleepScoreWidget(
sleepReport: sleepReport,
mediumLabel: "本月平均分",
), //睡眠评分
SleepChartContainer(
title: "每日得分",
tipText: "用户本月睡眠分数的汇总。",
sleepReport: sleepReport,
chartContent: LineView(
xLabels: [
ChartLables(
//标签系列1
min: 0, //最小值0
max: buildMonthlyChartData(
sleepReport['scoreList'])['daysInMonth'], //最大值30
q: buildMonthlyChartData(sleepReport['scoreList'])[
'daysInMonth'], //labels第一个与最后一个的真实距离也就是30-1 = 29
labels: List<String>.from(
sleepReport['scoreList']['xLable'].map((e) => e['name']),
),
indexs: (sleepReport['scoreList']['xLable'] as List)
.map((e) => e['index'] as int)
.toList(),
offset: Offset(0, -16.rpx), //标签相对于原点的偏移下方20像素位置
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style: TextStyle(color: Color(0XffD3D3D3), fontSize: 18.rpx),
);
},
),
],
yLabels: [
ChartLables(
min: sleepReport['scoreList']['yRange']['min'] ?? 0.0,
max: sleepReport['scoreList']['yRange']['max'] ?? 100.0,
q: 100,
labels: List<String>.from(
sleepReport['scoreList']['yLable']
.map((e) => e['name'].toString()),
),
offset: Offset(-15.rpx, 0),
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFFFFFFFF).withOpacity(0.6),
fontSize: 18.rpx),
);
},
),
],
tips: buildValueTexts(sleepReport['scoreList']['data'], '', 1),
xCount: buildMonthlyChartData(sleepReport['scoreList'])['daysInMonth']
.toInt(),
yCount: sleepReport['scoreList']['yLable'].length,
points: buildMonthlyChartData(sleepReport['scoreList'])['points'],
displayMode: ChartDisplayMode.bar,
barColors: buildMonthlyChartData(sleepReport['scoreList'])['colors'],
xUnit: sleepReport['scoreList']['yUnit'],
barWidth: 0.5,
),
showLabel: sleepReport['scoreList']['type'],
),
SleepCard(
sleepReport: sleepReport,
highlightItem: data['itemName'],
),
IndicatorCompareCard(
title: "与上月对比",
headers: ["名称", "上月", "本月", "参考范围"],
tooltip: "睡眠分数与上月分数进行对比,是通过量化分析近期睡眠质量变化,可了解自身睡眠状态的波动情况,进而调整用户的作息习惯。",
rows: (sleepReport['cwl'] ?? []).map<List<Widget>>((item) {
return [
Text(
item['name']?.toString() ?? '-',
style: TextStyle(color: Color(0xFFFFFFFF), fontSize: 26.rpx),
),
Text(
item['lastCurr']?.toString() ?? '-',
style: TextStyle(color: Color(0xFFFFFFFF), fontSize: 26.rpx),
),
Text(
item['curr']?.toString() ?? '-',
style: TextStyle(
color: item["currColor"] ?? Color(0xFFFFFFFF),
fontSize: 26.rpx,
),
),
Text(
item['range']?.toString() ?? '-',
style: TextStyle(color: Color(0xFFFFFFFF), fontSize: 26.rpx),
),
];
}).toList(),
),
SleepChartContainer(
title: "本月睡眠时长",
tipText: "本月睡眠时长是指从月初到月末,用户每天实际睡眠的时间总和。",
sleepReport: sleepReport,
chartContent: LineView(
xLabels: [
ChartLables(
//标签系列1
min: 0, //最小值0
max: buildMonthlyChartData(
sleepReport['scoreList'])['daysInMonth'], //最大值30
q: buildMonthlyChartData(sleepReport['scoreList'])[
'daysInMonth'], //labels第一个与最后一个的真实距离也就是30-1 = 29
labels: List<String>.from(
sleepReport['scoreList']['xLable'].map((e) => e['name']),
),
indexs: (sleepReport['scoreList']['xLable'] as List)
.map((e) => e['index'] as int)
.toList(),
offset: Offset(0, -16.rpx), //标签相对于原点的偏移下方20像素位置
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style: TextStyle(color: Color(0XffD3D3D3), fontSize: 18.rpx),
);
},
),
],
yLabels: [
ChartLables(
min: sleepReport['scoreList']['yRange']['min'] ?? 0.0,
max: sleepReport['scoreList']['yRange']['max'] ?? 10.0,
q: 10,
labels: List<String>.from(
sleepReport['csd']['yLable'].map((e) => e['name'].toString()),
),
offset: Offset(-15.rpx, 0),
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFFFFFFFF).withOpacity(0.6),
fontSize: 18.rpx),
);
},
),
],
tips: buildSleepValueTexts(sleepReport['csd']['data'], '小时', 1),
xCount: buildMonthlyChartData(sleepReport['scoreList'])['daysInMonth']
.toInt(),
yCount: sleepReport['csd']['yLable'].length,
points: [],
dualBarPoints: buildTripleBarData(sleepReport['csd']),
displayMode: ChartDisplayMode.dualBar,
xUnit: sleepReport['csd']['yUnit'],
barWidth: 0.5,
),
showLabel: sleepReport['csd']['type'],
),
TrendDataTablePage(
title: sleepReport['dysp'][0]['name'],
tipText: sleepReport['dysp'][0]['tips'],
chartContent: LineView(
xLabels: [
ChartLables(
//标签系列1
min: 0, //最小值0
max: buildMonthlyChartData(
sleepReport['scoreList'])['daysInMonth'], //最大值30
q: buildMonthlyChartData(sleepReport['scoreList'])[
'daysInMonth'], //labels第一个与最后一个的真实距离也就是30-1 = 29
labels: List<String>.from(
sleepReport['scoreList']['xLable'].map((e) => e['name']),
),
indexs: (sleepReport['scoreList']['xLable'] as List)
.map((e) => e['index'] as int)
.toList(),
offset: Offset(0, -16.rpx), //标签相对于原点的偏移下方20像素位置
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style:
TextStyle(color: Color(0XffD3D3D3), fontSize: 18.rpx),
);
},
),
],
yLabels: [
ChartLables(
min: 0,
max: 5,
q: 5,
labels: List<String>.from(
sleepReport['dysp'][0]['yLable']
.map((e) => e['name'].toString()),
),
offset: Offset(-30.rpx, 0),
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFFFFFFFF).withOpacity(0.6),
fontSize: 18.rpx),
);
},
),
],
tips: buildValueTexts(sleepReport['dysp'][0]['value'], '入睡时间:', 0),
xCount:
buildMonthlyChartData(sleepReport['scoreList'])['daysInMonth']
.toInt(),
yCount: sleepReport['dysp'][0]['yLable'].length,
points: buildGeneralPoints(sleepReport['dysp'][0]),
displayMode: ChartDisplayMode.line,
xUnit: sleepReport['dysp'][0]['yUnit'],
barWidth: 0.5,
bottomPadding: 16,
),
padding: 45.rpx),
TrendDataTablePage(
title: sleepReport['dysp'][1]['name'],
tipText: sleepReport['dysp'][1]['tips'],
chartContent: LineView(
xLabels: [
ChartLables(
//标签系列1
min: 0, //最小值0
max: buildMonthlyChartData(
sleepReport['scoreList'])['daysInMonth'], //最大值30
q: buildMonthlyChartData(sleepReport['scoreList'])[
'daysInMonth'], //labels第一个与最后一个的真实距离也就是30-1 = 29
labels: List<String>.from(
sleepReport['scoreList']['xLable'].map((e) => e['name']),
),
indexs: (sleepReport['scoreList']['xLable'] as List)
.map((e) => e['index'] as int)
.toList(),
offset: Offset(0, -16.rpx), //标签相对于原点的偏移下方20像素位置
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style:
TextStyle(color: Color(0XffD3D3D3), fontSize: 18.rpx),
);
},
),
],
yLabels: [
ChartLables(
min: 0,
max: 4,
q: 4,
labels: List<String>.from(
sleepReport['dysp'][1]['yLable']
.map((e) => e['name'].toString()),
),
offset: Offset(-30.rpx, 0),
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFFFFFFFF).withOpacity(0.6),
fontSize: 18.rpx),
);
},
),
],
tips: buildValueTexts(sleepReport['dysp'][1]['value'], '起床时间:', 0),
xCount:
buildMonthlyChartData(sleepReport['scoreList'])['daysInMonth']
.toInt(),
yCount: sleepReport['dysp'][1]['yLable'].length,
points: buildGeneralPoints(sleepReport['dysp'][1]),
displayMode: ChartDisplayMode.line,
xUnit: sleepReport['dysp'][1]['yUnit'],
barWidth: 0.5,
bottomPadding: 16,
),
padding: 45.rpx),
TrendDataTablePage(
title: sleepReport['dysp'][2]['name'],
tipText: sleepReport['dysp'][2]['tips'],
chartContent: LineView(
xLabels: [
ChartLables(
//标签系列1
min: 0, //最小值0
max: buildMonthlyChartData(
sleepReport['scoreList'])['daysInMonth'], //最大值30
q: buildMonthlyChartData(sleepReport['scoreList'])[
'daysInMonth'], //labels第一个与最后一个的真实距离也就是30-1 = 29
labels: List<String>.from(
sleepReport['scoreList']['xLable'].map((e) => e['name']),
),
indexs: (sleepReport['scoreList']['xLable'] as List)
.map((e) => e['index'] as int)
.toList(),
offset: Offset(0, -16.rpx), //标签相对于原点的偏移下方20像素位置
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style:
TextStyle(color: Color(0XffD3D3D3), fontSize: 18.rpx),
);
},
),
],
yLabels: [
ChartLables(
min: 0,
max: 4,
q: 4,
labels: List<String>.from(
sleepReport['dysp'][2]['yLable']
.map((e) => e['name'].toString()),
),
offset: Offset(-15.rpx, 0),
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFFFFFFFF).withOpacity(0.6),
fontSize: 18.rpx),
);
},
),
],
tips: buildValueTexts(sleepReport['dysp'][2]['value'], '', 1),
xCount:
buildMonthlyChartData(sleepReport['scoreList'])['daysInMonth']
.toInt(),
yCount: sleepReport['dysp'][2]['yLable'].length,
points: buildGeneralPoints(sleepReport['dysp'][2]),
displayMode: ChartDisplayMode.line,
xUnit: sleepReport['dysp'][2]['yUnit'],
barWidth: 0.5,
bottomPadding: 16,
),
padding: 45.rpx),
TrendDataTablePage(
title: sleepReport['dysp'][3]['name'],
tipText: sleepReport['dysp'][3]['tips'],
chartContent: LineView(
xLabels: [
ChartLables(
//标签系列1
min: 0, //最小值0
max: buildMonthlyChartData(
sleepReport['scoreList'])['daysInMonth'], //最大值30
q: buildMonthlyChartData(sleepReport['scoreList'])[
'daysInMonth'], //labels第一个与最后一个的真实距离也就是30-1 = 29
labels: List<String>.from(
sleepReport['scoreList']['xLable'].map((e) => e['name']),
),
indexs: (sleepReport['scoreList']['xLable'] as List)
.map((e) => e['index'] as int)
.toList(),
offset: Offset(0, -16.rpx), //标签相对于原点的偏移下方20像素位置
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style:
TextStyle(color: Color(0XffD3D3D3), fontSize: 18.rpx),
);
},
),
],
yLabels: [
ChartLables(
min: 0,
max: 5,
q: 5,
labels: List<String>.from(
sleepReport['dysp'][3]['yLable']
.map((e) => e['name'].toString()),
),
offset: Offset(-15.rpx, 0),
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFFFFFFFF).withOpacity(0.6),
fontSize: 18.rpx),
);
},
),
],
tips: buildValueTexts(sleepReport['dysp'][3]['value'], '毫秒', 1),
xCount:
buildMonthlyChartData(sleepReport['scoreList'])['daysInMonth']
.toInt(),
yCount: sleepReport['dysp'][3]['yLable'].length,
points: buildGeneralPoints(sleepReport['dysp'][3]),
displayMode: ChartDisplayMode.line,
xUnit: sleepReport['dysp'][3]['yUnit'],
barWidth: 0.5,
bottomPadding: 16,
),
padding: 45.rpx),
TrendDataTablePage(
title: sleepReport['dysp'][4]['name'],
tipText: sleepReport['dysp'][4]['tips'],
chartContent: LineView(
xLabels: [
ChartLables(
//标签系列1
min: 0, //最小值0
max: buildMonthlyChartData(
sleepReport['scoreList'])['daysInMonth'], //最大值30
q: buildMonthlyChartData(sleepReport['scoreList'])[
'daysInMonth'], //labels第一个与最后一个的真实距离也就是30-1 = 29
labels: List<String>.from(
sleepReport['scoreList']['xLable'].map((e) => e['name']),
),
indexs: (sleepReport['scoreList']['xLable'] as List)
.map((e) => e['index'] as int)
.toList(),
offset: Offset(0, -16.rpx), //标签相对于原点的偏移下方20像素位置
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style:
TextStyle(color: Color(0XffD3D3D3), fontSize: 18.rpx),
);
},
),
],
yLabels: [
ChartLables(
min: 0,
max: 5,
q: 5,
labels: List<String>.from(
sleepReport['dysp'][4]['yLable']
.map((e) => e['name'].toString()),
),
offset: Offset(-15.rpx, 0),
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFFFFFFFF).withOpacity(0.6),
fontSize: 18.rpx),
);
},
),
],
tips: buildValueTexts(sleepReport['dysp'][4]['value'], '次/分', 1),
xCount:
buildMonthlyChartData(sleepReport['scoreList'])['daysInMonth']
.toInt(),
yCount: sleepReport['dysp'][4]['yLable'].length,
points: buildGeneralPoints(sleepReport['dysp'][4]),
displayMode: ChartDisplayMode.line,
xUnit: sleepReport['dysp'][4]['yUnit'],
barWidth: 0.5,
bottomPadding: 16,
),
padding: 45.rpx),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("MAC号${data['mac']}",
style: TextStyle(
color: Color(0xFFD3D3D3).withOpacity(0.2), fontSize: 18.rpx))
],
),
]
.map((widget) => Padding(
padding: padding,
child: SizedBox(width: double.infinity, child: widget),
))
.toList();
}
return Column(
children: _buildSectionList(),
);
}
List<String> buildValueTexts(
List<dynamic> data,
String unit,
int direction, // 0 表示左侧1 表示右侧
) {
if (data.isEmpty) return [];
return data
.where((item) => item.containsKey('value') && item.containsKey('st'))
.map((item) {
final val = item['value'].toString();
// 单位位置
final prefix = direction == 1 ? '' : unit;
final suffix = direction == 1 ? unit : '';
// 中文年月日格式
DateTime date = DateTime.fromMillisecondsSinceEpoch(item['st']);
final dateStr = "${date.year}${date.month}${date.day}";
return "$prefix$val$suffix\n$dateStr";
}).toList();
}
List<String> buildSleepValueTexts(
List<dynamic> data,
String unit,
int direction, // 0 左侧1 右侧
) {
if (data.isEmpty) return [];
return data.map((item) {
final dst = (item['dst'] ?? 0).toString();
final lst = (item['lst'] ?? 0).toString();
final slt = (item['slt'] ?? 0).toString();
final prefix = direction == 1 ? '' : unit;
final suffix = direction == 1 ? unit : '';
// 格式化日期(不带时间)
String dateStr = '';
if (item['st'] != null) {
final dt = DateTime.fromMillisecondsSinceEpoch(item['st']);
dateStr =
"${dt.year}${dt.month.toString().padLeft(2, '0')}${dt.day.toString().padLeft(2, '0')}";
}
var q = [
"睡眠时长:$prefix$slt$suffix",
"深睡:$prefix$dst$suffix",
"浅睡:$prefix$lst$suffix",
dateStr,
].join("\n");
print(q);
return q;
}).toList();
}
Map<String, dynamic> buildMonthlyChartData(Map<String, dynamic> dyspData) {
final List<Map<String, dynamic>> data =
(dyspData['data'] as List?)?.whereType<Map<String, dynamic>>().toList() ??
[];
final List<Map<String, dynamic>> yLabels = (dyspData['yLable'] as List?)
?.whereType<Map<String, dynamic>>()
.toList() ??
[];
final List<Map<String, dynamic>> types =
(dyspData['type'] as List?)?.whereType<Map<String, dynamic>>().toList() ??
[];
if (data.isEmpty || yLabels.length < 2) {
return {
'points': <Offset>[],
'colors': <String>[],
'daysInMonth': 0.0,
};
}
// 解析 yLabel 为数值
List<double> yValues =
yLabels.map((e) => double.tryParse(e['name'].toString()) ?? 0).toList();
double minY = yValues.first;
double maxY = yValues.last;
double yRange = maxY - minY;
double yStepCount = (yValues.length - 1).toDouble(); // 👈 用 double
// 获取当前月的总天数(以第一条数据为准)
DateTime baseDate = DateTime.fromMillisecondsSinceEpoch(data.first['st']);
double daysInMonth =
DateTime(baseDate.year, baseDate.month + 1, 0).day.toDouble() -
1; // 👈 改为 double
List<Offset> points = [];
List<String> colors = [];
for (var item in data) {
final int st = item['st'];
final int level = item['level'];
final double value = (item['value'] as num).toDouble();
DateTime date = DateTime.fromMillisecondsSinceEpoch(st);
double day = date.day.toDouble();
// x: day - 10 起始)
double x = day - 1;
// // y: 归一化
// double y = ((value - minY) / yRange) * yStepCount;
// 获取 color
String color = types.firstWhere(
(e) => e['level'] == level,
orElse: () => {'color': '#CCCCCC'},
)['color'];
points.add(Offset(x, value));
colors.add(color);
}
return {
'points': points,
'colors': colors,
'daysInMonth': daysInMonth,
};
}
List<Offset> buildGeneralPoints(Map<String, dynamic> dyspData) {
final values =
(dyspData['value'] as List?)?.whereType<Map<String, dynamic>>().toList();
final yLabels =
(dyspData['yLable'] as List?)?.whereType<Map<String, dynamic>>().toList();
if (values == null || values.isEmpty || yLabels == null || yLabels.length < 2)
return [];
bool isTimeAxis = yLabels.first['name'].toString().contains(":");
double labelToValue(String name) {
if (isTimeAxis) {
final parts = name.split(':');
int hour = int.parse(parts[0]);
int minute = int.parse(parts[1]);
return hour * 60 + minute * 1.0;
} else {
return double.tryParse(name) ?? 0;
}
}
List<double> labelValues =
yLabels.map((e) => labelToValue(e['name'])).toList();
// 处理时间轴跨午夜
bool crossMidnight = false;
if (isTimeAxis && labelValues.last < labelValues.first) {
crossMidnight = true;
double base = labelValues.first;
for (int i = 1; i < labelValues.length; i++) {
if (labelValues[i] < base) {
labelValues[i] += 1440; // +24小时单位分钟
}
}
}
List<Offset> points = [];
for (var item in values) {
DateTime date = DateTime.fromMillisecondsSinceEpoch(item['st']);
double x = (date.day - 1).toDouble(); // 👈 按照“日”定位 X 坐标
dynamic rawValue = item['value'];
double valueMinBased;
if (isTimeAxis && rawValue is String) {
final parts = rawValue.split(':');
int hour = int.parse(parts[0]);
int minute = int.parse(parts[1]);
valueMinBased = hour * 60 + minute * 1.0;
if (crossMidnight && valueMinBased < labelValues.first) {
valueMinBased += 1440;
}
} else if (!isTimeAxis && rawValue is num) {
valueMinBased = rawValue.toDouble();
} else {
continue;
}
if (valueMinBased < labelValues.first) valueMinBased = labelValues.first;
if (valueMinBased > labelValues.last) valueMinBased = labelValues.last;
double y = getYPositionBySegmentedLabels(labelValues, valueMinBased);
points.add(Offset(x, y));
}
return points;
}
double getYPositionBySegmentedLabels(List<double> labels, double value) {
if (value <= labels.first) return 0.0;
if (value >= labels.last) return labels.length - 1.0;
for (int i = 0; i < labels.length - 1; i++) {
double low = labels[i];
double high = labels[i + 1];
if (value >= low && value <= high) {
double ratio = (value - low) / (high - low);
return i + ratio;
}
}
// 不应该走到这里默认返回0
return 0.0;
}
//多色柱状图坐标
List<List<Offset>> buildTripleBarData(Map<String, dynamic> csd) {
final List<Map<String, dynamic>> data =
(csd['data'] as List?)?.whereType<Map<String, dynamic>>().toList() ?? [];
final List<Map<String, dynamic>> yLabels =
(csd['yLable'] as List?)?.whereType<Map<String, dynamic>>().toList() ??
[];
if (data.isEmpty || yLabels.isEmpty) return [];
double minY = double.tryParse(yLabels.first['name'].toString()) ?? 0;
double maxY = double.tryParse(yLabels.last['name'].toString()) ?? 10;
List<List<Offset>> points = [];
for (var item in data) {
double dst = (item['dst'] as num?)?.toDouble() ?? 0;
double lst = (item['lst'] as num?)?.toDouble() ?? 0;
double slt = (item['slt'] as num?)?.toDouble() ?? 0;
// 限制 y 值范围
dst = dst.clamp(minY, maxY);
lst = lst.clamp(minY, maxY);
slt = slt.clamp(minY, maxY);
// 计算 X本月第几天从 0 开始)
DateTime date = DateTime.fromMillisecondsSinceEpoch(item['st']);
int dayOfMonth = date.day; // 1 ~ 31
double x = (dayOfMonth - 1).toDouble(); // 转为从 0 开始
points.add([
Offset(x, dst), // 深睡终点
Offset(x, dst + lst), // 浅睡+深睡终点
Offset(x, slt), // 总睡眠终点(第三段)
]);
}
return points;
}