added categories, import export, settings
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
/// Simple line chart of unit prices over time.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../models/purchase.dart';
|
||||
import '../utils/dates.dart';
|
||||
import '../utils/money.dart';
|
||||
|
||||
class PriceChart extends StatelessWidget {
|
||||
const PriceChart({super.key, required this.purchases});
|
||||
|
||||
final List<Purchase> purchases;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (purchases.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final points = purchases.reversed.toList();
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 160,
|
||||
child: CustomPaint(
|
||||
painter: _PriceChartPainter(
|
||||
prices: [
|
||||
for (final purchase in points) purchase.unitOrLinePrice,
|
||||
],
|
||||
lineColor: theme.colorScheme.primary,
|
||||
fillColor: theme.colorScheme.primary.withValues(alpha: 0.12),
|
||||
gridColor: theme.colorScheme.outlineVariant,
|
||||
labelColor: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
formatDate(points.first.boughtAt),
|
||||
style: theme.textTheme.labelMedium,
|
||||
),
|
||||
Text(
|
||||
formatDate(points.last.boughtAt),
|
||||
style: theme.textTheme.labelMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Latest ${formatEuro(points.last.unitOrLinePrice)}',
|
||||
style: theme.textTheme.labelMedium,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PriceChartPainter extends CustomPainter {
|
||||
_PriceChartPainter({
|
||||
required this.prices,
|
||||
required this.lineColor,
|
||||
required this.fillColor,
|
||||
required this.gridColor,
|
||||
required this.labelColor,
|
||||
});
|
||||
|
||||
final List<double> prices;
|
||||
final Color lineColor;
|
||||
final Color fillColor;
|
||||
final Color gridColor;
|
||||
final Color labelColor;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final minPrice = prices.reduce((a, b) => a < b ? a : b);
|
||||
final maxPrice = prices.reduce((a, b) => a > b ? a : b);
|
||||
final span = (maxPrice - minPrice).abs() < 0.01 ? 1.0 : maxPrice - minPrice;
|
||||
const left = 36.0;
|
||||
const right = 8.0;
|
||||
const top = 12.0;
|
||||
const bottom = 8.0;
|
||||
final chart = Rect.fromLTRB(left, top, size.width - right, size.height - bottom);
|
||||
|
||||
final gridPaint = Paint()
|
||||
..color = gridColor
|
||||
..strokeWidth = 1;
|
||||
for (var i = 0; i < 3; i++) {
|
||||
final y = chart.top + chart.height * i / 2;
|
||||
canvas.drawLine(Offset(chart.left, y), Offset(chart.right, y), gridPaint);
|
||||
}
|
||||
|
||||
final path = Path();
|
||||
final fill = Path();
|
||||
Offset? last;
|
||||
for (var i = 0; i < prices.length; i++) {
|
||||
final x = prices.length == 1
|
||||
? chart.center.dx
|
||||
: chart.left + chart.width * i / (prices.length - 1);
|
||||
final y = chart.bottom - chart.height * ((prices[i] - minPrice) / span);
|
||||
final point = Offset(x, y);
|
||||
last = point;
|
||||
if (i == 0) {
|
||||
path.moveTo(x, y);
|
||||
fill.moveTo(x, chart.bottom);
|
||||
fill.lineTo(x, y);
|
||||
} else {
|
||||
path.lineTo(x, y);
|
||||
fill.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
if (last != null) {
|
||||
fill.lineTo(last.dx, chart.bottom);
|
||||
fill.close();
|
||||
}
|
||||
|
||||
canvas.drawPath(fill, Paint()..color = fillColor);
|
||||
canvas.drawPath(
|
||||
path,
|
||||
Paint()
|
||||
..color = lineColor
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2.5
|
||||
..strokeCap = StrokeCap.round
|
||||
..strokeJoin = StrokeJoin.round,
|
||||
);
|
||||
|
||||
final dot = Paint()..color = lineColor;
|
||||
for (var i = 0; i < prices.length; i++) {
|
||||
final x = prices.length == 1
|
||||
? chart.center.dx
|
||||
: chart.left + chart.width * i / (prices.length - 1);
|
||||
final y = chart.bottom - chart.height * ((prices[i] - minPrice) / span);
|
||||
canvas.drawCircle(Offset(x, y), 3.5, dot);
|
||||
}
|
||||
|
||||
final labels = [maxPrice, (minPrice + maxPrice) / 2, minPrice];
|
||||
final textStyle = TextStyle(color: labelColor, fontSize: 10);
|
||||
for (var i = 0; i < labels.length; i++) {
|
||||
final tp = TextPainter(
|
||||
text: TextSpan(text: labels[i].toStringAsFixed(2), style: textStyle),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout(maxWidth: left - 4);
|
||||
final y = chart.top + chart.height * i / 2 - tp.height / 2;
|
||||
tp.paint(canvas, Offset(0, y));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _PriceChartPainter oldDelegate) =>
|
||||
oldDelegate.prices != prices || oldDelegate.lineColor != lineColor;
|
||||
}
|
||||
Reference in New Issue
Block a user