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 WeekDataWidget( Map sleepReport, dynamic data, ) { List _buildSectionList() { EdgeInsetsDirectional padding = EdgeInsetsDirectional.fromSTEB(30.rpx, 0, 30.rpx, 25.rpx); return [ AvgSleepScoreWidget( sleepReport: sleepReport, ), SleepChartContainer( title: "每日得分", tipText: "用户本周睡眠分数的汇总。", sleepReport: sleepReport, chartContent: LineView( xLabels: [ ChartLables( //标签系列1 min: 0, //最小值0 max: 7, //最大值30 q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29 labels: buildWeekDatesAndPoints(sleepReport['scoreList'])['dates'], indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置 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), ); }, ), ChartLables( //X轴标签系列2 min: 0, //最小值0 max: 7, //最大值30 q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29 labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'], indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置 offset: Offset(0, -50.rpx), //标签相对于原点的偏移,下方60像素位置 ondrawer: (canvas, offset, index, label, align, style) { ChartLables.drawText( canvas, label, offset, textAlign: TextAlign.center, style: TextStyle(color: Color(0XFFFFFFFF), fontSize: 26.rpx), ); }, ), ], yLabels: [ ChartLables( min: sleepReport['scoreList']['yRange']['min'] ?? 0.0, max: sleepReport['scoreList']['yRange']['max'] ?? 100.0, q: 100, labels: List.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), ); }, ), ], xCount: 7, yCount: sleepReport['scoreList']['yLable'].length, points: buildWeekDatesAndPoints(sleepReport['scoreList'])['points'], displayMode: ChartDisplayMode.bar, barColors: buildWeekDatesAndPoints(sleepReport['scoreList'])['colors'], tips: buildValueTexts(sleepReport['scoreList']['data'], '分', 1), xUnit: sleepReport['scoreList']['yUnit'], barWidth: 0.2, ), showLabel: sleepReport['scoreList']['type'], ), SleepCard( sleepReport: sleepReport, highlightItem: data['itemName'], ), IndicatorCompareCard( title: "与上周对比", headers: ["名称", "上周", "本周", "参考范围"], tooltip: "睡眠分数与上周分数进行对比,是通过量化分析近期睡眠质量变化,可了解自身睡眠状态的波动情况,进而调整用户的作息习惯。", rows: (sleepReport['cwl'] ?? []).map>((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: 7, //最大值30 q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29 labels: buildWeekDatesAndPoints(sleepReport['scoreList'])['dates'], indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置 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), ); }, ), ChartLables( //X轴标签系列2 min: 0, //最小值0 max: 7, //最大值30 q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29 labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'], indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置 offset: Offset(0, -50.rpx), //标签相对于原点的偏移,下方60像素位置 ondrawer: (canvas, offset, index, label, align, style) { ChartLables.drawText( canvas, label, offset, textAlign: TextAlign.center, style: TextStyle(color: Color(0XFFFFFFFF), fontSize: 26.rpx), ); }, ), ], yLabels: [ ChartLables( min: sleepReport['csd']['yRange']['min'] ?? 0.0, max: sleepReport['csd']['yRange']['max'] ?? 10.0, q: 10, labels: List.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), ); }, ), ], xCount: 7, yCount: sleepReport['csd']['yLable'].length, points: [], dualBarPoints: buildTripleBarData(sleepReport['csd']), displayMode: ChartDisplayMode.dualBar, xUnit: sleepReport['csd']['yUnit'], tips: buildSleepValueTexts(sleepReport['csd']['data'], '小时', 1), barWidth: 0.2, ), showLabel: sleepReport['csd']['type'], ), TrendDataTablePage( title: sleepReport['dysp'][0]['name'], tipText: sleepReport['dysp'][0]['tips'], chartContent: LineView( xLabels: [ ChartLables( //标签系列1 min: 0, //最小值0 max: 7, //最大值30 q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29 labels: buildWeekDatesAndPoints(sleepReport['scoreList'])['dates'], indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置 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), ); }, ), ChartLables( //X轴标签系列2 min: 0, //最小值0 max: 7, //最大值30 q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29 labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'], indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置 offset: Offset(0, -50.rpx), //标签相对于原点的偏移,下方60像素位置 ondrawer: (canvas, offset, index, label, align, style) { ChartLables.drawText( canvas, label, offset, textAlign: TextAlign.center, style: TextStyle(color: Color(0XFFFFFFFF), fontSize: 26.rpx), ); }, ), ], yLabels: [ ChartLables( min: 0, max: 5, q: 5, labels: List.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: 7, yCount: sleepReport['dysp'][0]['yLable'].length, points: buildGeneralPoints(sleepReport['dysp'][0]), displayMode: ChartDisplayMode.line, xUnit: sleepReport['dysp'][0]['yUnit'], barWidth: 0.1, bottomPadding: 50, ), padding: 45.rpx), TrendDataTablePage( title: sleepReport['dysp'][1]['name'], tipText: sleepReport['dysp'][1]['tips'], chartContent: LineView( xLabels: [ ChartLables( //标签系列1 min: 0, //最小值0 max: 7, //最大值30 q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29 labels: buildWeekDatesAndPoints(sleepReport['scoreList'])['dates'], indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置 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), ); }, ), ChartLables( //X轴标签系列2 min: 0, //最小值0 max: 7, //最大值30 q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29 labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'], indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置 offset: Offset(0, -50.rpx), //标签相对于原点的偏移,下方60像素位置 ondrawer: (canvas, offset, index, label, align, style) { ChartLables.drawText( canvas, label, offset, textAlign: TextAlign.center, style: TextStyle(color: Color(0XFFFFFFFF), fontSize: 26.rpx), ); }, ), ], yLabels: [ ChartLables( min: 0, max: 4, q: 4, labels: List.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: 7, yCount: sleepReport['dysp'][1]['yLable'].length, points: buildGeneralPoints(sleepReport['dysp'][1]), displayMode: ChartDisplayMode.line, xUnit: sleepReport['dysp'][1]['yUnit'], barWidth: 0.1, bottomPadding: 50, ), padding: 45.rpx), TrendDataTablePage( title: sleepReport['dysp'][2]['name'], tipText: sleepReport['dysp'][2]['tips'], chartContent: LineView( xLabels: [ ChartLables( //标签系列1 min: 0, //最小值0 max: 7, //最大值30 q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29 labels: buildWeekDatesAndPoints(sleepReport['scoreList'])['dates'], indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置 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), ); }, ), ChartLables( //X轴标签系列2 min: 0, //最小值0 max: 7, //最大值30 q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29 labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'], indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置 offset: Offset(0, -50.rpx), //标签相对于原点的偏移,下方60像素位置 ondrawer: (canvas, offset, index, label, align, style) { ChartLables.drawText( canvas, label, offset, textAlign: TextAlign.center, style: TextStyle(color: Color(0XFFFFFFFF), fontSize: 26.rpx), ); }, ), ], yLabels: [ ChartLables( min: 0, max: 4, q: 4, labels: List.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: 7, yCount: sleepReport['dysp'][2]['yLable'].length, points: buildGeneralPoints(sleepReport['dysp'][2]), displayMode: ChartDisplayMode.line, xUnit: sleepReport['dysp'][2]['yUnit'], barWidth: 0.1, bottomPadding: 50, ), padding: 45.rpx), TrendDataTablePage( title: sleepReport['dysp'][3]['name'], tipText: sleepReport['dysp'][3]['tips'], chartContent: LineView( xLabels: [ ChartLables( //标签系列1 min: 0, //最小值0 max: 7, //最大值30 q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29 labels: buildWeekDatesAndPoints(sleepReport['scoreList'])['dates'], indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置 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), ); }, ), ChartLables( //X轴标签系列2 min: 0, //最小值0 max: 7, //最大值30 q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29 labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'], indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置 offset: Offset(0, -50.rpx), //标签相对于原点的偏移,下方60像素位置 ondrawer: (canvas, offset, index, label, align, style) { ChartLables.drawText( canvas, label, offset, textAlign: TextAlign.center, style: TextStyle(color: Color(0XFFFFFFFF), fontSize: 26.rpx), ); }, ), ], yLabels: [ ChartLables( min: 0, max: 5, q: 5, labels: List.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: 7, yCount: sleepReport['dysp'][3]['yLable'].length, points: buildGeneralPoints(sleepReport['dysp'][3]), displayMode: ChartDisplayMode.line, xUnit: sleepReport['dysp'][3]['yUnit'], barWidth: 0.1, bottomPadding: 50, ), padding: 45.rpx), TrendDataTablePage( title: sleepReport['dysp'][4]['name'], tipText: sleepReport['dysp'][4]['tips'], chartContent: LineView( xLabels: [ ChartLables( //标签系列1 min: 0, //最小值0 max: 7, //最大值30 q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29 labels: buildWeekDatesAndPoints(sleepReport['scoreList'])['dates'], indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置 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), ); }, ), ChartLables( //X轴标签系列2 min: 0, //最小值0 max: 7, //最大值30 q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29 labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'], indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置 offset: Offset(0, -50.rpx), //标签相对于原点的偏移,下方60像素位置 ondrawer: (canvas, offset, index, label, align, style) { ChartLables.drawText( canvas, label, offset, textAlign: TextAlign.center, style: TextStyle(color: Color(0XFFFFFFFF), fontSize: 26.rpx), ); }, ), ], yLabels: [ ChartLables( min: 0, max: 5, q: 5, labels: List.from( sleepReport['dysp'][4]['yLable'] .map((e) => e['name'].toString()), ), offset: Offset(-20.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: 7, yCount: sleepReport['dysp'][4]['yLable'].length, points: buildGeneralPoints(sleepReport['dysp'][4]), displayMode: ChartDisplayMode.line, xUnit: sleepReport['dysp'][4]['yUnit'], barWidth: 0.1, bottomPadding: 50, ), 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(), ); } // 计算非均匀标签的 y 坐标 double getYPositionBySegmentedLabels(List 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 buildValueTexts( List data, String unit, int direction, // 0 表示左侧,1 表示右侧 ) { if (data.isEmpty) return []; return data.where((item) => item.containsKey('value')).map((item) { final val = item['value'].toString(); return direction == 1 ? "$val$unit" : "$unit$val"; }).toList(); } //多个关键点标签 List buildSleepValueTexts( List 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 : ''; var q = [ "睡眠时长:$prefix$slt$suffix", "深睡:$prefix$dst$suffix", "浅睡:$prefix$lst$suffix", ].join("\n"); print(q); return q; }).toList(); } int getWeekdayX(DateTime current, DateTime first) { final firstMonday = first.subtract(Duration(days: first.weekday - 1)); final currentMonday = current.subtract(Duration(days: current.weekday - 1)); final weekOffset = currentMonday.difference(firstMonday).inDays ~/ 7; return weekOffset * 7 + (current.weekday - 1); } Map buildWeekDatesAndPoints(Map scoreList) { if (!scoreList.containsKey('data') || (scoreList['data'] as List).isEmpty) { return { 'dates': [], 'points': [], 'colors': [], }; } List> data = (scoreList['data'] as List) .where((item) => item is Map) .cast>() .toList(); // 提取 level -> color 映射表 Map levelColorMap = {}; if (scoreList.containsKey('type')) { List typeList = scoreList['type']; for (var item in typeList) { if (item is Map && item.containsKey('level') && item.containsKey('color')) { levelColorMap[item['level']] = item['color']; } } } DateTime baseDate = DateTime.fromMillisecondsSinceEpoch(data.first['st']); int weekday = baseDate.weekday; DateTime monday = baseDate.subtract(Duration(days: weekday - 1)); List dates = List.generate(7, (i) { DateTime d = monday.add(Duration(days: i)); String month = d.month.toString().padLeft(2, '0'); String day = d.day.toString().padLeft(2, '0'); return "$month/$day"; }); List points = []; List colors = []; for (var item in data) { DateTime dt = DateTime.fromMillisecondsSinceEpoch(item['st']); double x = getWeekdayX(dt, baseDate).toDouble(); double y = (item['value'] as num?)?.toDouble() ?? 0; int level = item['level']; String color = levelColorMap[level] ?? "#FFFF00"; points.add(Offset(x, y)); colors.add(color); } return { 'dates': dates, 'points': points, 'colors': colors, }; } List buildGeneralPoints(Map dyspData) { final values = (dyspData['value'] as List?)?.whereType>().toList(); final yLabels = (dyspData['yLable'] as List?)?.whereType>().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 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; } } } DateTime baseDate = DateTime.fromMillisecondsSinceEpoch(values.first['st']); List points = []; for (var item in values) { DateTime date = DateTime.fromMillisecondsSinceEpoch(item['st']); double x = getWeekdayX(date, baseDate).toDouble(); 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; } List> buildTripleBarData(Map csd) { final data = (csd['data'] as List?)?.whereType>().toList() ?? []; final yLabels = (csd['yLable'] as List?)?.whereType>().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; DateTime firstDate = DateTime.fromMillisecondsSinceEpoch(data.first['st']); List> 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; dst = dst.clamp(minY, maxY); lst = lst.clamp(minY, maxY); slt = slt.clamp(minY, maxY); DateTime date = DateTime.fromMillisecondsSinceEpoch(item['st']); double x = getWeekdayX(date, firstDate).toDouble(); points.add([ Offset(x, dst), Offset(x, dst + lst), Offset(x, slt), ]); } return points; }