227 lines
7.6 KiB
Dart
227 lines
7.6 KiB
Dart
import 'package:fl_chart/fl_chart.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:vbvs_app/common/util/FitTool.dart';
|
||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||
|
||
class TimeSeriesPoint {
|
||
final int timestamp;
|
||
final double value;
|
||
TimeSeriesPoint(this.timestamp, this.value);
|
||
}
|
||
|
||
class TimeSeriesChart extends StatelessWidget {
|
||
final int startTime; // 毫秒时间戳
|
||
final int endTime;
|
||
final double yMin;
|
||
final double yMax;
|
||
final List<TimeSeriesPoint> dataPoints;
|
||
|
||
const TimeSeriesChart({
|
||
Key? key,
|
||
required this.startTime,
|
||
required this.endTime,
|
||
required this.yMin,
|
||
required this.yMax,
|
||
required this.dataPoints,
|
||
}) : super(key: key);
|
||
|
||
// X轴刻度数据
|
||
List<XLabel> _generateXLabels() {
|
||
final labels = <XLabel>[];
|
||
final startDate = DateTime.fromMillisecondsSinceEpoch(startTime);
|
||
final endDate = DateTime.fromMillisecondsSinceEpoch(endTime);
|
||
|
||
// 第一个刻度,原始 startTime,HH:mm格式
|
||
labels.add(XLabel(
|
||
time: startTime,
|
||
label:
|
||
'${startDate.hour.toString().padLeft(2, '0')}:${startDate.minute.toString().padLeft(2, '0')}',
|
||
));
|
||
|
||
// 生成中间整点小时刻度,注意起点向上取整一个小时
|
||
DateTime current = DateTime(
|
||
startDate.year,
|
||
startDate.month,
|
||
startDate.day,
|
||
startDate.hour,
|
||
);
|
||
if (startDate.minute > 0 ||
|
||
startDate.second > 0 ||
|
||
startDate.millisecond > 0) {
|
||
// 如果 startTime 不是整点,跳到下一个整点小时
|
||
current = current.add(Duration(hours: 1));
|
||
}
|
||
|
||
while (current.isBefore(endDate)) {
|
||
labels.add(XLabel(
|
||
time: current.millisecondsSinceEpoch,
|
||
label: current.hour.toString(),
|
||
));
|
||
current = current.add(Duration(hours: 1));
|
||
}
|
||
|
||
// 最后一个刻度,原始 endTime,HH:mm格式
|
||
labels.add(XLabel(
|
||
time: endTime,
|
||
label:
|
||
'${endDate.hour.toString().padLeft(2, '0')}:${endDate.minute.toString().padLeft(2, '0')}',
|
||
));
|
||
|
||
return labels;
|
||
}
|
||
|
||
// 时间戳映射到0~(labels.length-1)之间
|
||
double _timeToX(double timestamp, List<XLabel> labels) {
|
||
int start = labels.first.time;
|
||
int end = labels.last.time;
|
||
double total = (end - start).toDouble();
|
||
double pos = (timestamp - start).clamp(0, total).toDouble();
|
||
return pos / total * (labels.length - 1);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final xLabels = _generateXLabels();
|
||
final midY = (yMin + yMax) / 2;
|
||
|
||
List<FlSpot> spots = dataPoints.map((p) {
|
||
return FlSpot(_timeToX(p.timestamp.toDouble(), xLabels), p.value);
|
||
}).toList();
|
||
|
||
return AspectRatio(
|
||
aspectRatio: 2,
|
||
child: LineChart(
|
||
LineChartData(
|
||
minX: 0,
|
||
maxX: (xLabels.length - 1).toDouble(),
|
||
minY: yMin < 0 ? yMin : 0,
|
||
maxY: yMax,
|
||
gridData: FlGridData(show: false),
|
||
extraLinesData: ExtraLinesData(
|
||
horizontalLines: [
|
||
HorizontalLine(
|
||
y: 0,
|
||
color: themeController.currentColor.sc4,
|
||
strokeWidth: 1.rpx),
|
||
HorizontalLine(
|
||
y: yMin,
|
||
color: themeController.currentColor.sc9,
|
||
strokeWidth: 1.rpx,
|
||
dashArray: [5, 5]),
|
||
HorizontalLine(
|
||
y: yMax,
|
||
color: themeController.currentColor.sc9,
|
||
strokeWidth: 1.rpx,
|
||
dashArray: [5, 5]),
|
||
HorizontalLine(
|
||
y: midY,
|
||
color: themeController.currentColor.sc4,
|
||
strokeWidth: 1.rpx,
|
||
dashArray: [5, 5]),
|
||
],
|
||
),
|
||
titlesData: FlTitlesData(
|
||
bottomTitles: AxisTitles(
|
||
sideTitles: SideTitles(
|
||
showTitles: true,
|
||
reservedSize: 30,
|
||
interval: 1,
|
||
getTitlesWidget: (value, meta) {
|
||
int index = value.toInt();
|
||
if (index < 0 || index >= xLabels.length)
|
||
return const SizedBox.shrink();
|
||
|
||
final dateTime =
|
||
DateTime.fromMillisecondsSinceEpoch(xLabels[index].time);
|
||
|
||
if (index == 0 || index == xLabels.length - 1) {
|
||
// 开始和结束显示 HH:mm
|
||
final formatted =
|
||
'${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
|
||
return Padding(
|
||
padding: const EdgeInsets.only(top: 8.0),
|
||
child: Text(formatted,
|
||
style: TextStyle(
|
||
color: themeController.currentColor.sc4,
|
||
fontSize: 16.rpx)),
|
||
);
|
||
} else {
|
||
// 中间显示小时H,24小时制,不补零
|
||
final formatted = '${dateTime.hour}';
|
||
return Padding(
|
||
padding: const EdgeInsets.only(top: 8.0),
|
||
child: Text(formatted,
|
||
style: TextStyle(
|
||
color: themeController.currentColor.sc4,
|
||
fontSize: 16.rpx)),
|
||
);
|
||
}
|
||
},
|
||
),
|
||
),
|
||
leftTitles: AxisTitles(
|
||
sideTitles: SideTitles(
|
||
showTitles: true,
|
||
reservedSize: 40.rpx,
|
||
interval: 1,
|
||
getTitlesWidget: (value, meta) {
|
||
// 只显示 yMin, midY, yMax, 和 0
|
||
if ((value - 0).abs() < 0.01) {
|
||
return Text('0',
|
||
style: TextStyle(
|
||
color: themeController.currentColor.sc4,
|
||
fontSize: 16.rpx));
|
||
} else if ((value - yMin).abs() < 0.01) {
|
||
return Text(yMin.toStringAsFixed(0),
|
||
style: TextStyle(
|
||
color: themeController.currentColor.sc4,
|
||
fontSize: 16.rpx));
|
||
} else if ((value - midY).abs() < 0.01) {
|
||
return Text(midY.toStringAsFixed(0),
|
||
style: TextStyle(
|
||
color: themeController.currentColor.sc4,
|
||
fontSize: 16.rpx));
|
||
} else if ((value - yMax).abs() < 0.01) {
|
||
return Text(yMax.toStringAsFixed(0),
|
||
style: TextStyle(
|
||
color: themeController.currentColor.sc4,
|
||
fontSize: 16.rpx));
|
||
} else {
|
||
return const SizedBox.shrink();
|
||
}
|
||
},
|
||
),
|
||
),
|
||
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.none,
|
||
right: BorderSide.none,
|
||
top: BorderSide.none,
|
||
),
|
||
),
|
||
lineBarsData: [
|
||
LineChartBarData(
|
||
spots: spots,
|
||
isCurved: false,
|
||
color: themeController.currentColor.sc2,
|
||
barWidth: 2,
|
||
dotData: FlDotData(show: false),
|
||
)
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class XLabel {
|
||
final int time;
|
||
final String label;
|
||
XLabel({required this.time, required this.label});
|
||
}
|