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 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 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; }