991 lines
33 KiB
Dart
991 lines
33 KiB
Dart
// 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
|
||
// final double yAxisMargin; // Y轴上下边距,默认为2
|
||
|
||
// 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
|
||
// this.yAxisMargin = 2.0, // Y轴上下边距,默认为2
|
||
// }) : 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 - yAxisMargin,
|
||
// maxY: _actualYMax + yAxisMargin,
|
||
// 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 - yAxisMargin 到 yMax + yAxisMargin
|
||
// double xMin = 1;
|
||
// double xMaxValue = xMax;
|
||
// double yMinValue = yMin - yAxisMargin;
|
||
// double yMaxValue = yMax + yAxisMargin;
|
||
|
||
// 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,
|
||
// );
|
||
// }
|
||
// }
|
||
|
||
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
|
||
final double yAxisMargin; // Y轴上下边距,默认为2
|
||
|
||
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
|
||
this.yAxisMargin = 2.0, // Y轴上下边距,默认为2
|
||
}) : 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();
|
||
|
||
// 计算实际显示的Y轴范围
|
||
final double actualMinY = yMin - yAxisMargin;
|
||
final double actualMaxY = _actualYMax + yAxisMargin;
|
||
|
||
// 将数据点分割成多个连续段
|
||
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: actualMinY,
|
||
maxY: actualMaxY,
|
||
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) {
|
||
// 不显示最小值和最大值
|
||
// 判断是否是实际显示范围的最小值或最大值
|
||
if (value == actualMinY || value == actualMaxY) {
|
||
return const SizedBox.shrink();
|
||
}
|
||
|
||
// 检查值是否为整数(避免显示小数)
|
||
double roundedValue = value.roundToDouble();
|
||
if ((value - roundedValue).abs() > 0.01) {
|
||
// 如果不是整数,不显示
|
||
return const SizedBox.shrink();
|
||
}
|
||
|
||
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 - yAxisMargin 到 yMax + yAxisMargin
|
||
double xMin = 1;
|
||
double xMaxValue = xMax;
|
||
double yMinValue = yMin - yAxisMargin;
|
||
double yMaxValue = yMax + yAxisMargin;
|
||
|
||
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,
|
||
);
|
||
}
|
||
}
|