156 lines
4.3 KiB
Dart
156 lines
4.3 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'dart:math';
|
||
|
||
class ScoreProgressChart extends StatelessWidget {
|
||
// 历史成绩列表,按时间排序
|
||
final List<Map<String, dynamic>> historyScores;
|
||
final double height;
|
||
final Color lineColor;
|
||
final Color fillColor;
|
||
|
||
const ScoreProgressChart({
|
||
super.key,
|
||
required this.historyScores,
|
||
this.height = 60.0,
|
||
this.lineColor = Colors.blue,
|
||
this.fillColor = Colors.blueAccent,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
if (historyScores.isEmpty) {
|
||
return SizedBox(height: height);
|
||
}
|
||
|
||
// 提取达成率数据 (achievement / 10000)
|
||
// 假设 historyScores 已经按时间正序排列
|
||
final List<double> achievements = historyScores.map((s) {
|
||
int ach = s['achievement'] ?? 0;
|
||
return ach / 10000.0;
|
||
}).toList();
|
||
|
||
// 找出最小值和最大值以确定 Y 轴范围,为了美观,稍微扩大一点范围
|
||
double minVal = achievements.reduce(min);
|
||
double maxVal = achievements.reduce(max);
|
||
|
||
// 如果所有分数一样,给一个默认范围
|
||
if (minVal == maxVal) {
|
||
minVal = max(0, minVal - 1.0);
|
||
maxVal = min(101.0, maxVal + 1.0);
|
||
} else {
|
||
// 增加一点上下边距
|
||
double range = maxVal - minVal;
|
||
minVal = max(0, minVal - range * 0.1);
|
||
maxVal = min(101.0, maxVal + range * 0.1);
|
||
}
|
||
|
||
return SizedBox(
|
||
height: height,
|
||
child: CustomPaint(
|
||
size: Size.infinite,
|
||
painter: _ChartPainter(
|
||
achievements: achievements,
|
||
minVal: minVal,
|
||
maxVal: maxVal,
|
||
lineColor: lineColor,
|
||
fillColor: fillColor.withOpacity(0.2),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _ChartPainter extends CustomPainter {
|
||
final List<double> achievements;
|
||
final double minVal;
|
||
final double maxVal;
|
||
final Color lineColor;
|
||
final Color fillColor;
|
||
|
||
_ChartPainter({
|
||
required this.achievements,
|
||
required this.minVal,
|
||
required this.maxVal,
|
||
required this.lineColor,
|
||
required this.fillColor,
|
||
});
|
||
|
||
@override
|
||
void paint(Canvas canvas, Size size) {
|
||
// ✅ 修复1:数据不足直接返回
|
||
if (achievements.length < 1) return;
|
||
|
||
final paint = Paint()
|
||
..color = lineColor
|
||
..strokeWidth = 2.0
|
||
..style = PaintingStyle.stroke
|
||
..strokeCap = StrokeCap.round;
|
||
|
||
final fillPaint = Paint()
|
||
..color = fillColor
|
||
..style = PaintingStyle.fill;
|
||
|
||
final path = Path();
|
||
final fillPath = Path();
|
||
|
||
double width = size.width;
|
||
double height = size.height;
|
||
double range = maxVal - minVal;
|
||
if (range == 0) range = 1; // 防止除以0
|
||
|
||
// 计算每个点的坐标
|
||
List<Offset> points = [];
|
||
for (int i = 0; i < achievements.length; i++) {
|
||
double x;
|
||
if (achievements.length == 1) {
|
||
x = width / 2; // ✅ 修复2:只有1个点时,放中间
|
||
} else {
|
||
x = (i / (achievements.length - 1)) * width;
|
||
}
|
||
|
||
// Y轴翻转,因为Canvas原点在左上角
|
||
double normalizedY = (achievements[i] - minVal) / range;
|
||
double y = height - (normalizedY * height);
|
||
|
||
// ✅ 修复3:防止 NaN / 无限大
|
||
if (x.isNaN || x.isInfinite) x = 0;
|
||
if (y.isNaN || y.isInfinite) y = height / 2;
|
||
|
||
points.add(Offset(x, y));
|
||
}
|
||
|
||
// 构建折线路径
|
||
path.moveTo(points.first.dx, points.first.dy);
|
||
fillPath.moveTo(points.first.dx, height); // 填充从底部开始
|
||
fillPath.lineTo(points.first.dx, points.first.dy);
|
||
|
||
for (int i = 1; i < points.length; i++) {
|
||
path.lineTo(points[i].dx, points[i].dy);
|
||
fillPath.lineTo(points[i].dx, points[i].dy);
|
||
}
|
||
|
||
// 闭合填充路径
|
||
fillPath.lineTo(points.last.dx, height);
|
||
fillPath.close();
|
||
|
||
// 绘制填充
|
||
canvas.drawPath(fillPath, fillPaint);
|
||
// 绘制折线
|
||
canvas.drawPath(path, paint);
|
||
|
||
// 可选:绘制最后一个点的高亮圆点
|
||
final dotPaint = Paint()..color = lineColor;
|
||
final lastPoint = points.last;
|
||
// ✅ 修复4:绘制前校验,彻底杜绝NaN崩溃
|
||
if (!lastPoint.dx.isNaN && !lastPoint.dy.isNaN) {
|
||
canvas.drawCircle(lastPoint, 3.0, dotPaint);
|
||
}
|
||
}
|
||
|
||
@override
|
||
bool shouldRepaint(covariant _ChartPainter oldDelegate) {
|
||
return oldDelegate.achievements != achievements ||
|
||
oldDelegate.minVal != minVal ||
|
||
oldDelegate.maxVal != maxVal;
|
||
}
|
||
} |