107 lines
3.1 KiB
Dart
107 lines
3.1 KiB
Dart
import 'dart:math' as math;
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../theme/theme_x.dart';
|
|
|
|
/// A single-series trend line — currently just backup-duration history.
|
|
/// Hand-rolled `CustomPainter` rather than a charting dependency: one chart
|
|
/// in the whole app, a specific visual spec (faint grid, thin line,
|
|
/// emphasized endpoint), full control for near-zero cost.
|
|
class Sparkline extends StatelessWidget {
|
|
const Sparkline({super.key, required this.values, this.width = 240, this.height = 52});
|
|
|
|
final List<double> values;
|
|
final double width;
|
|
final double height;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SizedBox(
|
|
width: width,
|
|
height: height,
|
|
child: CustomPaint(
|
|
painter: _SparklinePainter(
|
|
values: values,
|
|
lineColor: context.colors.primary,
|
|
gridColor: context.colors.outlineVariant,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SparklinePainter extends CustomPainter {
|
|
_SparklinePainter({required this.values, required this.lineColor, required this.gridColor});
|
|
|
|
final List<double> values;
|
|
final Color lineColor;
|
|
final Color gridColor;
|
|
|
|
static const _topPad = 6.0;
|
|
static const _bottomPad = 6.0;
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
if (values.isEmpty) return;
|
|
|
|
final minV = values.reduce(math.min);
|
|
final maxV = values.reduce(math.max);
|
|
final range = (maxV - minV).abs() < 1e-9 ? 1.0 : maxV - minV;
|
|
final plotHeight = size.height - _topPad - _bottomPad;
|
|
|
|
final gridPaint = Paint()
|
|
..color = gridColor
|
|
..strokeWidth = 1;
|
|
canvas.drawLine(Offset(0, _topPad), Offset(size.width, _topPad), gridPaint);
|
|
canvas.drawLine(
|
|
Offset(0, size.height - _bottomPad),
|
|
Offset(size.width, size.height - _bottomPad),
|
|
gridPaint,
|
|
);
|
|
|
|
if (values.length == 1) {
|
|
final y = _topPad + plotHeight / 2;
|
|
canvas.drawCircle(Offset(size.width, y), 3.4, Paint()..color = lineColor);
|
|
return;
|
|
}
|
|
|
|
final dx = size.width / (values.length - 1);
|
|
final points = [
|
|
for (var i = 0; i < values.length; i++)
|
|
Offset(i * dx, _topPad + plotHeight - ((values[i] - minV) / range) * plotHeight),
|
|
];
|
|
|
|
final fillPath = Path()..moveTo(points.first.dx, size.height - _bottomPad);
|
|
for (final p in points) {
|
|
fillPath.lineTo(p.dx, p.dy);
|
|
}
|
|
fillPath
|
|
..lineTo(points.last.dx, size.height - _bottomPad)
|
|
..close();
|
|
canvas.drawPath(fillPath, Paint()..color = lineColor.withValues(alpha: 0.14));
|
|
|
|
final linePath = Path()..moveTo(points.first.dx, points.first.dy);
|
|
for (final p in points.skip(1)) {
|
|
linePath.lineTo(p.dx, p.dy);
|
|
}
|
|
canvas.drawPath(
|
|
linePath,
|
|
Paint()
|
|
..color = lineColor
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = 2
|
|
..strokeCap = StrokeCap.round
|
|
..strokeJoin = StrokeJoin.round,
|
|
);
|
|
|
|
canvas.drawCircle(points.last, 3.4, Paint()..color = lineColor);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant _SparklinePainter oldDelegate) =>
|
|
oldDelegate.values != values ||
|
|
oldDelegate.lineColor != lineColor ||
|
|
oldDelegate.gridColor != gridColor;
|
|
}
|