add graph_view

This commit is contained in:
2026-01-26 16:12:38 +01:00
parent 78e27ac8d0
commit 8e612037d0
9 changed files with 573 additions and 1 deletions

View File

@@ -0,0 +1,328 @@
import 'package:flutter/material.dart';
import '../models/chart_model.dart';
class ChartLineWidget extends StatelessWidget {
final ChartData chartData;
const ChartLineWidget({super.key, required this.chartData});
@override
Widget build(BuildContext context) {
if (chartData.dataPoints.isEmpty) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16.0),
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Colors.blueGrey[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blueGrey[200]!),
),
child: const Center(child: Text('Keine Daten verfügbar')),
);
}
final maxLiters = chartData.getMaxLiters();
final maxPrice = chartData.getMaxPrice();
final maxPricePerLiter = chartData.getMaxPricePerLiter();
final maxY = [
maxLiters,
maxPrice,
maxPricePerLiter,
].reduce((a, b) => a > b ? a : b);
final padding = maxY * 0.1;
final yAxisMax = maxY + padding;
return Column(
children: [
Container(
margin: const EdgeInsets.symmetric(horizontal: 16.0),
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Colors.blueGrey[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blueGrey[200]!),
),
child: Column(
children: [
SizedBox(
height: 300,
child: CustomPaint(
painter: LineChartPainter(
chartData: chartData,
yAxisMax: yAxisMax,
),
size: Size.infinite,
),
),
const SizedBox(height: 20),
Wrap(
spacing: 20,
runSpacing: 10,
children: [
_LegendItem(color: Colors.red, label: 'Liter pro Stop'),
_LegendItem(color: Colors.green, label: 'Preis pro Stop (€)'),
_LegendItem(
color: Colors.amber,
label: 'Preis pro Liter (€)',
),
],
),
],
),
),
],
);
}
}
class _LegendItem extends StatelessWidget {
final Color color;
final String label;
const _LegendItem({required this.color, required this.label});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 16,
height: 16,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
const SizedBox(width: 8),
Text(
label,
style: const TextStyle(fontSize: 12, color: Colors.black87),
),
],
);
}
}
class LineChartPainter extends CustomPainter {
final ChartData chartData;
final double yAxisMax;
LineChartPainter({required this.chartData, required this.yAxisMax});
@override
void paint(Canvas canvas, Size size) {
if (chartData.dataPoints.isEmpty) return;
final paint = Paint()
..color = Colors.blueGrey[300]!
..strokeWidth = 1;
final axisPaint = Paint()
..color = Colors.blueGrey[600]!
..strokeWidth = 2;
final padding = EdgeInsets.fromLTRB(40, 20, 20, 40);
final graphWidth = size.width - padding.left - padding.right;
final graphHeight = size.height - padding.top - padding.bottom;
// Draw grid and axes
_drawAxes(canvas, size, padding, axisPaint, paint);
// Draw lines
_drawLine(
canvas,
chartData,
padding,
graphWidth,
graphHeight,
Colors.red,
(point) => point.litersPerStop,
);
_drawLine(
canvas,
chartData,
padding,
graphWidth,
graphHeight,
Colors.green,
(point) => point.pricePerStop,
);
_drawLine(
canvas,
chartData,
padding,
graphWidth,
graphHeight,
Colors.amber,
(point) => point.pricePerLiter,
);
// Draw points
final pointPaint = Paint()
..strokeWidth = 3
..strokeCap = StrokeCap.round;
final pointsSize = 4.0;
for (var point in chartData.dataPoints) {
final xPos =
padding.left +
(point.stopNumber - 1) *
(graphWidth /
(chartData.dataPoints.length - 1 > 0
? chartData.dataPoints.length - 1
: 1));
// Red points (Liters)
final yLiters =
padding.top +
graphHeight -
(point.litersPerStop / yAxisMax) * graphHeight;
pointPaint.color = Colors.red;
canvas.drawCircle(Offset(xPos, yLiters), pointsSize, pointPaint);
// Green points (Price)
final yPrice =
padding.top +
graphHeight -
(point.pricePerStop / yAxisMax) * graphHeight;
pointPaint.color = Colors.green;
canvas.drawCircle(Offset(xPos, yPrice), pointsSize, pointPaint);
// Amber points (Price per Liter)
final yPricePerLiter =
padding.top +
graphHeight -
(point.pricePerLiter / yAxisMax) * graphHeight;
pointPaint.color = Colors.amber;
canvas.drawCircle(Offset(xPos, yPricePerLiter), pointsSize, pointPaint);
}
}
void _drawAxes(
Canvas canvas,
Size size,
EdgeInsets padding,
Paint axisPaint,
Paint gridPaint,
) {
// Y-axis
canvas.drawLine(
Offset(padding.left, padding.top),
Offset(padding.left, size.height - padding.bottom),
axisPaint,
);
// X-axis
canvas.drawLine(
Offset(padding.left, size.height - padding.bottom),
Offset(size.width - padding.right, size.height - padding.bottom),
axisPaint,
);
final graphHeight = size.height - padding.top - padding.bottom;
final graphWidth = size.width - padding.left - padding.right;
// Y-axis labels
final steps = 5;
for (int i = 0; i <= steps; i++) {
final yPos = padding.top + (graphHeight / steps) * i;
final value = yAxisMax - (yAxisMax / steps) * i;
// Grid line
canvas.drawLine(
Offset(padding.left, yPos),
Offset(size.width - padding.right, yPos),
gridPaint..strokeWidth = 0.5,
);
// Label
final textPainter = TextPainter(
text: TextSpan(
text: value.toStringAsFixed(1),
style: const TextStyle(color: Colors.black54, fontSize: 10),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(
padding.left - textPainter.width - 8,
yPos - textPainter.height / 2,
),
);
}
// X-axis labels
for (int i = 0; i < chartData.dataPoints.length; i++) {
final xPos =
padding.left +
i *
(graphWidth /
(chartData.dataPoints.length - 1 > 0
? chartData.dataPoints.length - 1
: 1));
final textPainter = TextPainter(
text: TextSpan(
text: (i + 1).toString(),
style: const TextStyle(color: Colors.black54, fontSize: 10),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(xPos - textPainter.width / 2, size.height - padding.bottom + 8),
);
}
}
void _drawLine(
Canvas canvas,
ChartData chartData,
EdgeInsets padding,
double graphWidth,
double graphHeight,
Color color,
double Function(ChartDataPoint) getValue,
) {
final paint = Paint()
..color = color
..strokeWidth = 2
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..style = PaintingStyle.stroke;
final points = chartData.dataPoints;
if (points.length < 2) return;
final path = Path();
for (int i = 0; i < points.length; i++) {
final x =
padding.left +
i * (graphWidth / (points.length - 1 > 0 ? points.length - 1 : 1));
final y =
padding.top +
graphHeight -
(getValue(points[i]) / yAxisMax) * graphHeight;
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(LineChartPainter oldDelegate) {
return oldDelegate.chartData != chartData ||
oldDelegate.yAxisMax != yAxisMax;
}
}