Files

107 lines
3.1 KiB
Dart
Raw Permalink Normal View History

2026-07-27 14:42:23 +02:00
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;
}