329 lines
8.5 KiB
Dart
329 lines
8.5 KiB
Dart
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;
|
|
}
|
|
}
|