253 lines
7.0 KiB
Dart
253 lines
7.0 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:intl/intl.dart';
|
||
import 'package:vbvs_app/common/util/FitTool.dart';
|
||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||
import 'dart:ui' as ui;
|
||
import 'dart:math';
|
||
|
||
class LineChartByRange extends StatelessWidget {
|
||
final List<Map<String, dynamic>> showLabel;
|
||
final int startTime;
|
||
final int endTime;
|
||
|
||
const LineChartByRange({
|
||
Key? key,
|
||
required this.showLabel,
|
||
required this.startTime,
|
||
required this.endTime,
|
||
}) : super(key: key);
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
if (showLabel.isEmpty) return const SizedBox();
|
||
|
||
int maxTimes =
|
||
showLabel.map((e) => e['times'] ?? 0).reduce((a, b) => a > b ? a : b);
|
||
int yMax = (maxTimes / 10).ceil() * 10;
|
||
if (yMax == 0) yMax = 10;
|
||
|
||
DateTime minTime = DateTime.fromMillisecondsSinceEpoch(startTime);
|
||
DateTime maxTime = DateTime.fromMillisecondsSinceEpoch(endTime);
|
||
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
SizedBox(
|
||
height: 500.rpx,
|
||
child: CustomPaint(
|
||
size: Size(double.infinity, 500.rpx),
|
||
painter: _LineChartByRangePainter(
|
||
data: showLabel,
|
||
yMax: yMax,
|
||
minTime: minTime,
|
||
maxTime: maxTime,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _LineChartByRangePainter extends CustomPainter {
|
||
final List<Map<String, dynamic>> data;
|
||
final int yMax;
|
||
final DateTime minTime;
|
||
final DateTime maxTime;
|
||
|
||
_LineChartByRangePainter({
|
||
required this.data,
|
||
required this.yMax,
|
||
required this.minTime,
|
||
required this.maxTime,
|
||
});
|
||
|
||
@override
|
||
void paint(Canvas canvas, Size size) {
|
||
double padding = 20.rpx;
|
||
double labelInset = 12.rpx; // X轴标签缩进距离
|
||
|
||
// 绘图X轴起止点,考虑内缩labelInset
|
||
final double xStart = padding + labelInset;
|
||
final double xEnd = size.width - padding - labelInset;
|
||
final double chartWidth = xEnd - xStart;
|
||
|
||
double chartHeight = size.height - 30.rpx;
|
||
|
||
int totalDuration =
|
||
maxTime.millisecondsSinceEpoch - minTime.millisecondsSinceEpoch;
|
||
if (totalDuration <= 0) return;
|
||
|
||
Paint linePaint = Paint()
|
||
..style = PaintingStyle.stroke
|
||
..strokeWidth = 3.rpx
|
||
..color = stringToColor("#00C1AA")
|
||
..strokeCap = StrokeCap.round;
|
||
|
||
Paint fillCirclePaint = Paint()
|
||
..style = PaintingStyle.fill
|
||
..color = stringToColor("#00C1AA");
|
||
|
||
// 1. 先绘制数据线段及起止点圆点
|
||
for (var item in data) {
|
||
int start = item['startTime'];
|
||
int end = item['endTime'];
|
||
int times = item['times'];
|
||
|
||
double startX = xStart +
|
||
chartWidth * (start - minTime.millisecondsSinceEpoch) / totalDuration;
|
||
double endX = xStart +
|
||
chartWidth * (end - minTime.millisecondsSinceEpoch) / totalDuration;
|
||
double y = chartHeight * (1 - times / yMax);
|
||
|
||
// 画线段
|
||
canvas.drawLine(Offset(startX, y), Offset(endX, y), linePaint);
|
||
|
||
// 画起点圆点
|
||
canvas.drawCircle(Offset(startX, y), 4.rpx, fillCirclePaint);
|
||
|
||
// 画终点圆点
|
||
canvas.drawCircle(Offset(endX, y), 4.rpx, fillCirclePaint);
|
||
}
|
||
|
||
// 2. Y轴辅助线及文字
|
||
Paint axisPaint = Paint()
|
||
..color = Colors.grey.withOpacity(0.4)
|
||
..strokeWidth = 1.rpx;
|
||
|
||
for (int i = 0; i <= 6; i++) {
|
||
double y = chartHeight * i / 6;
|
||
|
||
if (i == 6) {
|
||
// 实线
|
||
canvas.drawLine(Offset(xStart, y), Offset(xEnd, y), axisPaint);
|
||
} else {
|
||
// 虚线
|
||
drawDashedLine(
|
||
canvas,
|
||
Offset(xStart, y),
|
||
Offset(xEnd, y),
|
||
axisPaint,
|
||
dashWidth: 8.rpx,
|
||
dashSpace: 6.rpx,
|
||
);
|
||
}
|
||
|
||
// Y轴文字
|
||
TextPainter tp = TextPainter(
|
||
text: TextSpan(
|
||
text: '${yMax - (yMax * i / 6).round()}',
|
||
style: TextStyle(
|
||
fontSize: 18.rpx, color: themeController.currentColor.sc4),
|
||
),
|
||
textDirection: ui.TextDirection.ltr,
|
||
);
|
||
tp.layout();
|
||
tp.paint(canvas, Offset(0, y - tp.height / 2));
|
||
}
|
||
|
||
// 3. X轴线
|
||
canvas.drawLine(
|
||
Offset(xStart, chartHeight), Offset(xEnd, chartHeight), axisPaint);
|
||
|
||
// 4. 画X轴时间点对应的垂直虚线辅助线
|
||
int totalHours = maxTime.difference(minTime).inHours;
|
||
int startHour = minTime.hour;
|
||
|
||
// for (int i = 1; i < totalHours; i++) {
|
||
// double x = xStart + chartWidth * i / totalHours;
|
||
|
||
// // 垂直虚线
|
||
// drawDashedLine(
|
||
// canvas,
|
||
// Offset(x, 0),
|
||
// Offset(x, chartHeight),
|
||
// axisPaint,
|
||
// dashWidth: 4.rpx,
|
||
// dashSpace: 4.rpx,
|
||
// );
|
||
// }
|
||
|
||
// 5. 画左侧完整时分 (HH:mm),往内缩 labelInset
|
||
String leftLabel = DateFormat('HH:mm').format(minTime);
|
||
TextPainter leftTp = TextPainter(
|
||
text: TextSpan(
|
||
text: leftLabel,
|
||
style: TextStyle(
|
||
fontSize: 18.rpx,
|
||
color: themeController.currentColor.sc4,
|
||
),
|
||
),
|
||
textDirection: ui.TextDirection.ltr,
|
||
);
|
||
leftTp.layout();
|
||
leftTp.paint(canvas,
|
||
Offset(padding + labelInset - leftTp.width / 2, chartHeight + 8.rpx));
|
||
|
||
// 6. 画右侧完整时分 (HH:mm),往内缩 labelInset
|
||
String rightLabel = DateFormat('HH:mm').format(maxTime);
|
||
TextPainter rightTp = TextPainter(
|
||
text: TextSpan(
|
||
text: rightLabel,
|
||
style: TextStyle(
|
||
fontSize: 18.rpx,
|
||
color: themeController.currentColor.sc4,
|
||
),
|
||
),
|
||
textDirection: ui.TextDirection.ltr,
|
||
);
|
||
rightTp.layout();
|
||
rightTp.paint(
|
||
canvas,
|
||
Offset(size.width - padding - labelInset - rightTp.width / 2,
|
||
chartHeight + 8.rpx));
|
||
|
||
// 7. 中间小时数字(23, 0, 1, 2, ...)
|
||
for (int i = 1; i < totalHours; i++) {
|
||
double x = xStart + chartWidth * i / totalHours;
|
||
|
||
int hourLabelNum = (startHour + i) % 24;
|
||
String hourLabel = '$hourLabelNum';
|
||
|
||
TextPainter tp = TextPainter(
|
||
text: TextSpan(
|
||
text: hourLabel,
|
||
style: TextStyle(
|
||
fontSize: 18.rpx,
|
||
color: themeController.currentColor.sc4,
|
||
),
|
||
),
|
||
textDirection: ui.TextDirection.ltr,
|
||
);
|
||
tp.layout();
|
||
|
||
tp.paint(canvas, Offset(x - tp.width / 2, chartHeight + 8.rpx));
|
||
}
|
||
}
|
||
|
||
@override
|
||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||
|
||
void drawDashedLine(
|
||
Canvas canvas,
|
||
Offset start,
|
||
Offset end,
|
||
Paint paint, {
|
||
required double dashWidth,
|
||
required double dashSpace,
|
||
}) {
|
||
final dx = end.dx - start.dx;
|
||
final dy = end.dy - start.dy;
|
||
final distance = sqrt(dx * dx + dy * dy);
|
||
final direction = Offset(dx / distance, dy / distance);
|
||
|
||
double drawn = 0;
|
||
while (drawn < distance) {
|
||
final from = start + direction * drawn;
|
||
final to = start + direction * (drawn + dashWidth).clamp(0, distance);
|
||
canvas.drawLine(from, to, paint);
|
||
drawn += dashWidth + dashSpace;
|
||
}
|
||
}
|
||
}
|