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 pvMap = { for (var v in pvValues) (v.datetime ?? ''): (v.value ?? 0.0), }; final Map 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 _generateBarGroups( List datetimes, Map pvMap, Map 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; }