Files
flutter_test_services/lib/widgets/comparison_bar_chart.dart
T
2026-07-23 09:17:56 +02:00

334 lines
12 KiB
Dart

import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import '../models/response_output.dart';
/// Wiederverwendbares Balkendiagramm Widget für PV/Import Vergleich
///
/// Zeigt zwei Datensätze (z.B. TraunPV + TraunImport) als nebeneinander angeordnete Balken
/// - PV Daten in Orange
/// - Import Daten in Rot
/// - Y-Achse: kW Werte
/// - X-Achse: Datetime (als Uhrzeit angezeigt)
class ComparisonBarChart extends StatelessWidget {
final ResponseOutput pvData;
final ResponseOutput importData;
final String title;
const ComparisonBarChart({
required this.pvData,
required this.importData,
required this.title,
super.key,
});
@override
Widget build(BuildContext context) {
final pvValues = pvData.dataResult?.values ?? [];
final importValues = importData.dataResult?.values ?? [];
// Erstelle eine Map mit datetime als Schlüssel
final Map<String, double> pvMap = {
for (var v in pvValues) (v.datetime ?? ''): (v.value ?? 0.0),
};
final Map<String, double> importMap = {
for (var v in importValues) (v.datetime ?? ''): (v.value ?? 0.0),
};
// Alle eindeutigen datetimes sammeln und sortieren
final allDatetimes = {...pvMap.keys, ...importMap.keys}.toList();
allDatetimes.sort();
// Begrenzen auf die letzten 20 Einträge für bessere Lesbarkeit
// final displayDatetimes = allDatetimes.length > 20
// ? allDatetimes.sublist(allDatetimes.length - 20)
// : allDatetimes;
final displayDatetimes = allDatetimes;
if (displayDatetimes.isEmpty) {
return const Center(child: Text('Keine Daten verfügbar'));
}
// Berechne Maximalwert
final allValues = [...pvMap.values, ...importMap.values];
final maxValue = allValues.fold(0.0, (a, b) => a > b ? a : b);
// Maxima für PV und Import separat
double maxPvValue = 0;
int maxPvIndex = 0;
double maxImportValue = 0;
int maxImportIndex = 0;
for (int i = 0; i < displayDatetimes.length; i++) {
final dt = displayDatetimes[i];
final pv = pvMap[dt] ?? 0.0;
final imp = importMap[dt] ?? 0.0;
if (pv > maxPvValue) {
maxPvValue = pv;
maxPvIndex = i;
}
if (imp > maxImportValue) {
maxImportValue = imp;
maxImportIndex = i;
}
}
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 16),
Text(
title,
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
// Chart mit fester Höhe
SizedBox(
height: 500,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: maxValue * 1.02,
minY: 0,
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: maxValue * 0.2,
),
titlesData: FlTitlesData(
show: true,
topTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
getTitlesWidget: (value, meta) {
final index = value.toInt();
// PV Maximum - oranger Pin
if (index == maxPvIndex) {
return Transform.translate(
offset: const Offset(-5, 0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
decoration: BoxDecoration(
color: Colors.orange,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.3),
blurRadius: 4,
),
],
),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
child: Text(
maxPvValue.toStringAsFixed(0),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
// Pin nach unten
CustomPaint(
size: const Size(20, 10),
painter: PinPainter(Colors.orange),
),
],
),
);
}
// Import Maximum - roter Pin
if (index == maxImportIndex) {
return Transform.translate(
offset: const Offset(5, 0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.3),
blurRadius: 4,
),
],
),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
child: Text(
maxImportValue.toStringAsFixed(0),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
// Pin nach unten
CustomPaint(
size: const Size(20, 10),
painter: PinPainter(Colors.red),
),
],
),
);
}
return const SizedBox();
},
),
),
rightTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 50,
getTitlesWidget: (value, meta) {
return Text(
'${value.toInt()} kW',
style: const TextStyle(fontSize: 12),
);
},
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 60,
getTitlesWidget: (value, meta) {
final index = value.toInt();
if (index < 0 || index >= displayDatetimes.length) {
return const SizedBox();
}
final datetime = displayDatetimes[index];
// Extrahiere nur Uhrzeit (HH:mm)
final timePart = datetime.contains('T')
? datetime.split('T').last.substring(0, 5)
: datetime.substring(0, 5);
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: RotatedBox(
quarterTurns: 1,
child: Text(
timePart,
style: const TextStyle(fontSize: 10),
),
),
);
},
),
),
),
barGroups: _generateBarGroups(
displayDatetimes,
pvMap,
importMap,
),
borderData: FlBorderData(show: true),
),
),
),
const SizedBox(height: 24),
// Legende
Wrap(
alignment: WrapAlignment.center,
spacing: 24,
runSpacing: 8,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(width: 16, height: 16, color: Colors.orange),
const SizedBox(width: 8),
const Text('PV-Anlage(kW)'),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(width: 16, height: 16, color: Colors.red),
const SizedBox(width: 8),
const Text('Netzbezug(kW)'),
],
),
],
),
const SizedBox(height: 16),
],
),
),
);
}
List<BarChartGroupData> _generateBarGroups(
List<String> datetimes,
Map<String, double> pvMap,
Map<String, double> importMap,
) {
return List.generate(datetimes.length, (index) {
final datetime = datetimes[index];
final pvValue = pvMap[datetime] ?? 0.0;
final importValue = importMap[datetime] ?? 0.0;
return BarChartGroupData(
x: index,
barRods: [
// Orange Bar für PV
BarChartRodData(
toY: pvValue,
color: Colors.orange,
width: 8,
borderRadius: const BorderRadius.vertical(top: Radius.circular(4)),
),
// Roter Bar für Import
BarChartRodData(
toY: importValue,
color: Colors.red,
width: 8,
borderRadius: const BorderRadius.vertical(top: Radius.circular(4)),
),
],
);
});
}
}
class PinPainter extends CustomPainter {
final Color color;
PinPainter(this.color);
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = color;
final path = Path();
path.moveTo(size.width / 2, size.height);
path.lineTo(0, 0);
path.lineTo(size.width, 0);
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(PinPainter oldDelegate) => false;
}