Files
tuiche/lib/pages/sleep_report/chart/QcTimeSeriesChart.dart
2026-03-10 12:01:00 +08:00

978 lines
32 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/ef.dart';
// import 'package:fl_chart/fl_chart.dart';
// 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 QcTimeSeriesPoint {
// final double value;
// QcTimeSeriesPoint(this.value);
// }
// class QcTimeSeriesChart extends StatelessWidget {
// final List<QcTimeSeriesPoint> dataPoints;
// final double yMin;
// final double yMax;
// final int xSegmentCount;
// final double? baseValue; // 基准值,可选
// final String? baseLabel; // 基准值标签,可选
// const QcTimeSeriesChart({
// Key? key,
// required this.yMin,
// required this.yMax,
// required this.dataPoints,
// this.xSegmentCount = 11,
// this.baseValue,
// this.baseLabel,
// }) : super(key: key);
// int get _dataPointCount => dataPoints.length;
// List<double> _generateYAxisTicks() {
// if (yMin >= yMax) {
// return [0, 20, 40, 60, 80, 100];
// }
// double step = (yMax - yMin) / 5;
// List<double> ticks = [];
// for (int i = 0; i <= 5; i++) {
// ticks.add(yMin + (step * i));
// }
// return ticks;
// }
// // 计算所有刻度位置对应的索引
// List<int> _getTickIndices() {
// List<int> tickIndices = [];
// // 确保至少显示两个刻度(起点和终点)
// if (_dataPointCount <= 1) {
// return [0];
// }
// // 计算合理的刻度间隔
// double step = _dataPointCount / (xSegmentCount - 1);
// for (int i = 0; i < xSegmentCount; i++) {
// int index = (i * step).round();
// // 确保索引在有效范围内
// index = index.clamp(0, _dataPointCount - 1);
// // 避免重复的索引
// if (tickIndices.isEmpty || index != tickIndices.last) {
// tickIndices.add(index);
// }
// }
// // 确保最后一个点是最后一个数据点
// if (tickIndices.last != _dataPointCount - 1) {
// tickIndices.add(_dataPointCount - 1);
// }
// return tickIndices;
// }
// // 查找最大值和最小值的索引和值
// Map<String, dynamic> _findMinMax() {
// if (dataPoints.isEmpty)
// return {'minIndex': -1, 'maxIndex': -1, 'minValue': 0, 'maxValue': 0};
// int minIndex = -1;
// int maxIndex = -1;
// double minValue = double.infinity;
// double maxValue = -double.infinity;
// for (int i = 0; i < dataPoints.length; i++) {
// var point = dataPoints[i];
// if (point.value != -1) {
// // 跳过无效数据
// if (point.value < minValue) {
// minValue = point.value;
// minIndex = i;
// }
// if (point.value > maxValue) {
// maxValue = point.value;
// maxIndex = i;
// }
// }
// }
// return {
// 'minIndex': minIndex,
// 'maxIndex': maxIndex,
// 'minValue': minValue,
// 'maxValue': maxValue
// };
// }
// @override
// Widget build(BuildContext context) {
// // final yTicks = _generateYAxisTicks();
// final double xMax = _dataPointCount.toDouble();
// final double yRange = yMax - yMin;
// final double yInterval = yRange > 0 ? yRange / 5 : 20.0;
// final tickIndices = _getTickIndices();
// final minMaxData = _findMinMax();
// // 将数据点分割成多个连续段
// List<List<FlSpot>> lineSegments = [];
// List<FlSpot> currentSegment = [];
// for (int i = 0; i < dataPoints.length; i++) {
// var point = dataPoints[i];
// if (point.value != -1) {
// currentSegment.add(FlSpot(
// (i + 1).toDouble(),
// point.value,
// ));
// } else if (currentSegment.isNotEmpty) {
// lineSegments.add(currentSegment);
// currentSegment = [];
// }
// }
// if (currentSegment.isNotEmpty) {
// lineSegments.add(currentSegment);
// }
// // 创建渐变填充的线图数据
// List<LineChartBarData> lineBarsData = [];
// for (var segment in lineSegments) {
// if (segment.isEmpty) continue;
// lineBarsData.add(
// LineChartBarData(
// spots: segment,
// isCurved: false,
// color: themeController.currentColor.sc2,
// barWidth: 2,
// dotData: FlDotData(show: false),
// preventCurveOverShooting: true,
// belowBarData: BarAreaData(
// show: true,
// gradient: LinearGradient(
// begin: Alignment.topCenter,
// end: Alignment.bottomCenter,
// colors: [
// themeController.currentColor.sc2.withOpacity(0.3),
// themeController.currentColor.sc2.withOpacity(0.1),
// Colors.transparent,
// ],
// stops: const [0.0, 1, 1.0],
// ),
// applyCutOffY: true,
// cutOffY: 0,
// ),
// ),
// );
// }
// // 创建刻度点的数据(绿色小球)
// List<FlSpot> tickSpots = [];
// for (int index in tickIndices) {
// if (index >= 0 && index < dataPoints.length) {
// var point = dataPoints[index];
// if (point.value != -1) {
// tickSpots.add(FlSpot(
// (index + 1).toDouble(),
// point.value,
// ));
// }
// }
// }
// // 准备水平线列表
// List<HorizontalLine> horizontalLines = [
// HorizontalLine(
// y: 0,
// color: themeController.currentColor.sc4,
// strokeWidth: 1.rpx,
// ),
// ];
// // 如果有基准值,添加基准线
// if (baseValue != null) {
// horizontalLines.add(
// HorizontalLine(
// y: baseValue!,
// color: themeController.currentColor.sc9,
// strokeWidth: 2.rpx,
// dashArray: [8, 4], // 虚线样式
// label: HorizontalLineLabel(
// show: true,
// alignment: Alignment.topRight,
// labelResolver: (line) =>
// "基准".tr + '${baseValue!.toStringAsFixed(0)}',
// style: TextStyle(
// color: themeController.currentColor.sc9,
// fontSize: 18.rpx,
// fontWeight: FontWeight.w500,
// ),
// ),
// ),
// );
// }
// return AspectRatio(
// aspectRatio: 2,
// child: LayoutBuilder(
// builder: (context, constraints) {
// return Stack(
// children: [
// LineChart(
// LineChartData(
// minX: 1,
// maxX: xMax + 0.5,
// minY: yMin - 2,
// maxY: yMax + 2,
// gridData: FlGridData(show: false),
// extraLinesData: ExtraLinesData(
// horizontalLines: horizontalLines,
// ),
// titlesData: FlTitlesData(
// bottomTitles: AxisTitles(
// sideTitles: SideTitles(
// showTitles: true,
// reservedSize: 30,
// interval: 1, // 让 fl_chart 自动计算合适的间隔
// getTitlesWidget: (value, meta) {
// // 只显示我们在 tickIndices 中定义的刻度
// int index = value.round() - 1;
// if (index >= 0 &&
// index < _dataPointCount &&
// tickIndices.contains(index)) {
// String label;
// if (index == 0) {
// label = '0';
// } else if (index == _dataPointCount - 1) {
// label = '${_dataPointCount}';
// } else {
// label = '${index + 1}';
// }
// return Padding(
// padding: const EdgeInsets.only(top: 8.0),
// child: Text(
// label,
// style: TextStyle(
// color: themeController.currentColor.sc4,
// fontSize: 14.rpx,
// ),
// ),
// );
// }
// return const SizedBox.shrink();
// },
// ),
// ),
// leftTitles: AxisTitles(
// sideTitles: SideTitles(
// showTitles: true,
// reservedSize: 60.rpx,
// interval: yInterval,
// getTitlesWidget: (value, meta) {
// return Padding(
// padding: const EdgeInsets.only(right: 8.0),
// child: Text(
// value.toStringAsFixed(0),
// style: TextStyle(
// color: themeController.currentColor.sc4,
// fontSize: 16.rpx,
// ),
// textAlign: TextAlign.right,
// ),
// );
// },
// ),
// ),
// rightTitles:
// AxisTitles(sideTitles: SideTitles(showTitles: false)),
// topTitles:
// AxisTitles(sideTitles: SideTitles(showTitles: false)),
// ),
// borderData: FlBorderData(
// show: true,
// border: Border(
// bottom: BorderSide(color: Colors.grey.withOpacity(0.3)),
// left: BorderSide(color: Colors.grey.withOpacity(0.3)),
// right: BorderSide.none,
// top: BorderSide.none,
// ),
// ),
// lineBarsData: [
// ...lineBarsData,
// // 添加绿色小球的线图数据
// LineChartBarData(
// spots: tickSpots,
// isCurved: false,
// color: Colors.transparent,
// barWidth: 0,
// dotData: FlDotData(
// show: true,
// getDotPainter: (spot, percent, barData, index) {
// return FlDotCirclePainter(
// radius: 4.rpx,
// color: stringToColor("#5DD8C9"),
// strokeWidth: 1,
// strokeColor: stringToColor("#00C1AA"),
// );
// },
// ),
// preventCurveOverShooting: true,
// ),
// ],
// ),
// ),
// // 使用 Positioned 来绘制最大值和最小值的标签
// if (minMaxData['minIndex'] != -1 || minMaxData['maxIndex'] != -1)
// _buildMinMaxLabels(
// context, constraints, minMaxData, xMax, yMin, yMax),
// ],
// );
// },
// ),
// );
// }
// Widget _buildMinMaxLabels(
// BuildContext context,
// BoxConstraints constraints,
// Map<String, dynamic> minMaxData,
// double xMax,
// double yMin,
// double yMax,
// ) {
// // 获取图表区域的实际绘制区域
// // fl_chart 默认会有一些内边距,我们需要估算这些内边距
// double leftPadding = 60.rpx; // 左侧留白用于Y轴标签
// double rightPadding = 10.rpx; // 右侧留白
// double topPadding = 10.rpx; // 顶部留白
// double bottomPadding = 30.rpx; // 底部留白用于X轴标签
// double chartWidth = constraints.maxWidth - leftPadding - rightPadding;
// double chartHeight = constraints.maxHeight - topPadding - bottomPadding;
// // X轴范围1 到 xMax
// // Y轴范围yMin - 2 到 yMax + 2
// double xMin = 1;
// double xMaxValue = xMax;
// double yMinValue = yMin - 2;
// double yMaxValue = yMax + 2;
// double xRange = xMaxValue - xMin;
// double yRange = yMaxValue - yMinValue;
// List<Widget> labels = [];
// // 添加最小值标签
// if (minMaxData['minIndex'] != -1) {
// double minX = (minMaxData['minIndex'] + 1).toDouble();
// double minY = minMaxData['minValue'];
// // 计算在图表中的相对位置 (0-1)
// double relativeX = (minX - xMin) / xRange;
// double relativeY = (minY - yMinValue) / yRange;
// // 转换为像素位置
// double left = leftPadding + (relativeX * chartWidth);
// double top = topPadding + ((1 - relativeY) * chartHeight);
// // 标签尺寸
// double labelWidth = 38.rpx;
// double labelHeight = 50.rpx;
// labels.add(
// Positioned(
// left: left - labelWidth / 2, // 水平居中
// top: top - labelHeight - 8.rpx, // 显示在点的正上方留出8rpx间距
// child: SizedBox(
// width: labelWidth,
// height: labelHeight,
// child: Stack(
// alignment: Alignment.center,
// children: [
// // SVG图片作为背景
// SvgPicture.asset(
// 'assets/img/icon/location.svg', // 替换为你的SVG图片路径
// // width: 28.rpx,
// // height: 40.rpx,
// fit: BoxFit.contain,
// color: stringToColor("#d69dd2"),
// ),
// Padding(
// padding: EdgeInsets.only(bottom: 12.rpx), // 调整这个值来控制向上移动的距离
// child: Text(
// '${minMaxData['minValue'].toStringAsFixed(0)}',
// style: TextStyle(
// color: Colors.white,
// fontSize: AppConstants().smaller_text_fontSize,
// ),
// ),
// ),
// ],
// ),
// ),
// ),
// );
// }
// // 添加最大值标签
// if (minMaxData['maxIndex'] != -1 &&
// minMaxData['maxIndex'] != minMaxData['minIndex']) {
// double maxX = (minMaxData['maxIndex'] + 1).toDouble();
// double maxY = minMaxData['maxValue'];
// // 计算在图表中的相对位置 (0-1)
// double relativeX = (maxX - xMin) / xRange;
// double relativeY = (maxY - yMinValue) / yRange;
// // 转换为像素位置
// double left = leftPadding + (relativeX * chartWidth);
// double top = topPadding + ((1 - relativeY) * chartHeight);
// // 标签尺寸
// double labelWidth = 38.rpx;
// double labelHeight = 50.rpx;
// labels.add(
// Positioned(
// left: left - labelWidth / 2, // 水平居中
// top: top - labelHeight - 8.rpx, // 显示在点的正上方留出8rpx间距
// child: SizedBox(
// width: labelWidth,
// height: labelHeight,
// child: Stack(
// alignment: Alignment.center,
// children: [
// // SVG图片作为背景
// SvgPicture.asset(
// 'assets/img/icon/location.svg', // 替换为你的SVG图片路径
// // width: 28.rpx,
// // height: 40.rpx,
// fit: BoxFit.contain,
// color: stringToColor("#FF9F66"),
// ),
// // 文字覆盖在SVG上
// // Text(
// // '${minMaxData['maxValue'].toStringAsFixed(0)}',
// // style: TextStyle(
// // color: Colors.white,
// // fontSize: AppConstants().smaller_text_fontSize,
// // // fontWeight: FontWeight.bold,
// // ),
// // ),
// Padding(
// padding: EdgeInsets.only(bottom: 12.rpx), // 调整这个值来控制向上移动的距离
// child: Text(
// '${minMaxData['maxValue'].toStringAsFixed(0)}',
// style: TextStyle(
// color: Colors.white,
// fontSize: AppConstants().smaller_text_fontSize,
// ),
// ),
// ),
// ],
// ),
// ),
// ),
// );
// }
// return Stack(
// children: labels,
// );
// }
// }
import 'package:ef/ef.dart';
import 'package:fl_chart/fl_chart.dart';
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 QcTimeSeriesPoint {
final double value;
QcTimeSeriesPoint(this.value);
}
class QcTimeSeriesChart extends StatelessWidget {
final List<QcTimeSeriesPoint> dataPoints;
final double yMin;
final double yMax; // 注意:这个值在使用时会自动加 padding
final int xSegmentCount;
final double? baseValue; // 基准值,可选
final String? baseLabel; // 基准值标签,可选
final double yAxisPadding; // Y轴顶部留白默认为20
const QcTimeSeriesChart({
Key? key,
required this.yMin,
required this.yMax,
required this.dataPoints,
this.xSegmentCount = 11,
this.baseValue,
this.baseLabel,
this.yAxisPadding = 20.0, // 默认为20
}) : super(key: key);
// 添加一个 getter 来获取实际使用的 yMax自动加 padding
double get _actualYMax => yMax + yAxisPadding;
int get _dataPointCount => dataPoints.length;
List<double> _generateYAxisTicks() {
if (yMin >= _actualYMax) {
return [0, 20, 40, 60, 80, 100];
}
double step = (_actualYMax - yMin) / 5;
List<double> ticks = [];
for (int i = 0; i <= 5; i++) {
ticks.add(yMin + (step * i));
}
return ticks;
}
// 计算所有刻度位置对应的索引
List<int> _getTickIndices() {
List<int> tickIndices = [];
// 确保至少显示两个刻度(起点和终点)
if (_dataPointCount <= 1) {
return [0];
}
// 计算合理的刻度间隔
double step = _dataPointCount / (xSegmentCount - 1);
for (int i = 0; i < xSegmentCount; i++) {
int index = (i * step).round();
// 确保索引在有效范围内
index = index.clamp(0, _dataPointCount - 1);
// 避免重复的索引
if (tickIndices.isEmpty || index != tickIndices.last) {
tickIndices.add(index);
}
}
// 确保最后一个点是最后一个数据点
if (tickIndices.last != _dataPointCount - 1) {
tickIndices.add(_dataPointCount - 1);
}
return tickIndices;
}
// 查找最大值和最小值的索引和值
Map<String, dynamic> _findMinMax() {
if (dataPoints.isEmpty)
return {'minIndex': -1, 'maxIndex': -1, 'minValue': 0, 'maxValue': 0};
int minIndex = -1;
int maxIndex = -1;
double minValue = double.infinity;
double maxValue = -double.infinity;
for (int i = 0; i < dataPoints.length; i++) {
var point = dataPoints[i];
if (point.value != -1) {
// 跳过无效数据
if (point.value < minValue) {
minValue = point.value;
minIndex = i;
}
if (point.value > maxValue) {
maxValue = point.value;
maxIndex = i;
}
}
}
return {
'minIndex': minIndex,
'maxIndex': maxIndex,
'minValue': minValue,
'maxValue': maxValue
};
}
@override
Widget build(BuildContext context) {
// final yTicks = _generateYAxisTicks();
final double xMax = _dataPointCount.toDouble();
final double yRange = _actualYMax - yMin;
final double yInterval = yRange > 0 ? yRange / 5 : 20.0;
final tickIndices = _getTickIndices();
final minMaxData = _findMinMax();
// 将数据点分割成多个连续段
List<List<FlSpot>> lineSegments = [];
List<FlSpot> currentSegment = [];
for (int i = 0; i < dataPoints.length; i++) {
var point = dataPoints[i];
if (point.value != -1) {
currentSegment.add(FlSpot(
(i + 1).toDouble(),
point.value,
));
} else if (currentSegment.isNotEmpty) {
lineSegments.add(currentSegment);
currentSegment = [];
}
}
if (currentSegment.isNotEmpty) {
lineSegments.add(currentSegment);
}
// 创建渐变填充的线图数据
List<LineChartBarData> lineBarsData = [];
for (var segment in lineSegments) {
if (segment.isEmpty) continue;
lineBarsData.add(
LineChartBarData(
spots: segment,
isCurved: false,
color: themeController.currentColor.sc2,
barWidth: 2,
dotData: FlDotData(show: false),
preventCurveOverShooting: true,
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
themeController.currentColor.sc2.withOpacity(0.3),
themeController.currentColor.sc2.withOpacity(0.1),
Colors.transparent,
],
stops: const [0.0, 1, 1.0],
),
applyCutOffY: true,
cutOffY: 0,
),
),
);
}
// 创建刻度点的数据(绿色小球)
List<FlSpot> tickSpots = [];
for (int index in tickIndices) {
if (index >= 0 && index < dataPoints.length) {
var point = dataPoints[index];
if (point.value != -1) {
tickSpots.add(FlSpot(
(index + 1).toDouble(),
point.value,
));
}
}
}
// 准备水平线列表
List<HorizontalLine> horizontalLines = [
HorizontalLine(
y: 0,
color: themeController.currentColor.sc4,
strokeWidth: 1.rpx,
),
];
// 如果有基准值,添加基准线
if (baseValue != null) {
horizontalLines.add(
HorizontalLine(
y: baseValue!,
color: themeController.currentColor.sc9,
strokeWidth: 2.rpx,
dashArray: [8, 4], // 虚线样式
label: HorizontalLineLabel(
show: true,
alignment: Alignment.topRight,
labelResolver: (line) =>
"基准".tr + '${baseValue!.toStringAsFixed(0)}',
style: TextStyle(
color: themeController.currentColor.sc9,
fontSize: 18.rpx,
fontWeight: FontWeight.w500,
),
),
),
);
}
return AspectRatio(
aspectRatio: 2,
child: LayoutBuilder(
builder: (context, constraints) {
return Stack(
children: [
LineChart(
LineChartData(
minX: 1,
maxX: xMax + 0.5,
minY: yMin - 2,
maxY: _actualYMax + 2,
gridData: FlGridData(show: false),
extraLinesData: ExtraLinesData(
horizontalLines: horizontalLines,
),
titlesData: FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
interval: 1, // 让 fl_chart 自动计算合适的间隔
getTitlesWidget: (value, meta) {
// 只显示我们在 tickIndices 中定义的刻度
int index = value.round() - 1;
if (index >= 0 &&
index < _dataPointCount &&
tickIndices.contains(index)) {
String label;
if (index == 0) {
label = '0';
} else if (index == _dataPointCount - 1) {
label = '${_dataPointCount}';
} else {
label = '${index + 1}';
}
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
label,
style: TextStyle(
color: themeController.currentColor.sc4,
fontSize: 14.rpx,
),
),
);
}
return const SizedBox.shrink();
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 60.rpx,
interval: yInterval,
getTitlesWidget: (value, meta) {
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Text(
value.toStringAsFixed(0),
style: TextStyle(
color: themeController.currentColor.sc4,
fontSize: 16.rpx,
),
textAlign: TextAlign.right,
),
);
},
),
),
rightTitles:
AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles:
AxisTitles(sideTitles: SideTitles(showTitles: false)),
),
borderData: FlBorderData(
show: true,
border: Border(
bottom: BorderSide(color: Colors.grey.withOpacity(0.3)),
left: BorderSide(color: Colors.grey.withOpacity(0.3)),
right: BorderSide.none,
top: BorderSide.none,
),
),
lineBarsData: [
...lineBarsData,
// 添加绿色小球的线图数据
LineChartBarData(
spots: tickSpots,
isCurved: false,
color: Colors.transparent,
barWidth: 0,
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
return FlDotCirclePainter(
radius: 4.rpx,
color: stringToColor("#5DD8C9"),
strokeWidth: 1,
strokeColor: stringToColor("#00C1AA"),
);
},
),
preventCurveOverShooting: true,
),
],
),
),
// 使用 Positioned 来绘制最大值和最小值的标签
if (minMaxData['minIndex'] != -1 || minMaxData['maxIndex'] != -1)
_buildMinMaxLabels(
context, constraints, minMaxData, xMax, yMin, _actualYMax),
],
);
},
),
);
}
Widget _buildMinMaxLabels(
BuildContext context,
BoxConstraints constraints,
Map<String, dynamic> minMaxData,
double xMax,
double yMin,
double yMax,
) {
// 获取图表区域的实际绘制区域
// fl_chart 默认会有一些内边距,我们需要估算这些内边距
double leftPadding = 60.rpx; // 左侧留白用于Y轴标签
double rightPadding = 10.rpx; // 右侧留白
double topPadding = 10.rpx; // 顶部留白
double bottomPadding = 30.rpx; // 底部留白用于X轴标签
double chartWidth = constraints.maxWidth - leftPadding - rightPadding;
double chartHeight = constraints.maxHeight - topPadding - bottomPadding;
// X轴范围1 到 xMax
// Y轴范围yMin - 2 到 yMax + 2
double xMin = 1;
double xMaxValue = xMax;
double yMinValue = yMin - 2;
double yMaxValue = yMax + 2;
double xRange = xMaxValue - xMin;
double yRange = yMaxValue - yMinValue;
List<Widget> labels = [];
// 添加最小值标签
if (minMaxData['minIndex'] != -1) {
double minX = (minMaxData['minIndex'] + 1).toDouble();
double minY = minMaxData['minValue'];
// 计算在图表中的相对位置 (0-1)
double relativeX = (minX - xMin) / xRange;
double relativeY = (minY - yMinValue) / yRange;
// 转换为像素位置
double left = leftPadding + (relativeX * chartWidth);
double top = topPadding + ((1 - relativeY) * chartHeight);
// 标签尺寸
double labelWidth = 38.rpx;
double labelHeight = 50.rpx;
labels.add(
Positioned(
left: left - labelWidth / 2, // 水平居中
top: top - labelHeight - 8.rpx, // 显示在点的正上方留出8rpx间距
child: SizedBox(
width: labelWidth,
height: labelHeight,
child: Stack(
alignment: Alignment.center,
children: [
// SVG图片作为背景
SvgPicture.asset(
'assets/img/icon/location.svg',
fit: BoxFit.contain,
color: stringToColor("#d69dd2"),
),
Padding(
padding: EdgeInsets.only(bottom: 12.rpx),
child: Text(
'${minMaxData['minValue'].toStringAsFixed(0)}',
style: TextStyle(
color: Colors.white,
fontSize: AppConstants().smaller_text_fontSize,
),
),
),
],
),
),
),
);
}
// 添加最大值标签
if (minMaxData['maxIndex'] != -1 &&
minMaxData['maxIndex'] != minMaxData['minIndex']) {
double maxX = (minMaxData['maxIndex'] + 1).toDouble();
double maxY = minMaxData['maxValue'];
// 计算在图表中的相对位置 (0-1)
double relativeX = (maxX - xMin) / xRange;
double relativeY = (maxY - yMinValue) / yRange;
// 转换为像素位置
double left = leftPadding + (relativeX * chartWidth);
double top = topPadding + ((1 - relativeY) * chartHeight);
// 标签尺寸
double labelWidth = 38.rpx;
double labelHeight = 50.rpx;
labels.add(
Positioned(
left: left - labelWidth / 2, // 水平居中
top: top - labelHeight - 8.rpx, // 显示在点的正上方留出8rpx间距
child: SizedBox(
width: labelWidth,
height: labelHeight,
child: Stack(
alignment: Alignment.center,
children: [
// SVG图片作为背景
SvgPicture.asset(
'assets/img/icon/location.svg',
fit: BoxFit.contain,
color: stringToColor("#FF9F66"),
),
Padding(
padding: EdgeInsets.only(bottom: 12.rpx),
child: Text(
'${minMaxData['maxValue'].toStringAsFixed(0)}',
style: TextStyle(
color: Colors.white,
fontSize: AppConstants().smaller_text_fontSize,
),
),
),
],
),
),
),
);
}
return Stack(
children: labels,
);
}
}