initial commit
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user