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.only( left: 26.0, right: 35.0, top: 16.0, bottom: 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( barTouchData: BarTouchData( enabled: true, touchTooltipData: BarTouchTooltipData( // Optional: Hintergrundfarbe und Styling des Tooltips anpassen getTooltipItem: (group, groupIndex, rod, rodIndex) { // 1. Datum/Uhrzeit anhand des groupIndex abrufen final rawDateTime = displayDatetimes[groupIndex]; // 2. Formatieren zu "20.07.26 12:00" String formattedDateTime = rawDateTime; try { final parsedDate = DateTime.parse(rawDateTime); final day = parsedDate.day.toString().padLeft(2, '0'); final month = parsedDate.month.toString().padLeft( 2, '0', ); final year = parsedDate.year.toString().substring(2); final hour = parsedDate.hour.toString().padLeft( 2, '0', ); final minute = parsedDate.minute.toString().padLeft( 2, '0', ); formattedDateTime = '$day.$month.$year $hour:$minute'; } catch (_) {} // 3. Typ bestimmen (z.B. PV oder Import je nach Rod-Index) final label = rodIndex == 0 ? 'PV' : 'Import'; final unit = 'kW'; // 4. Nur beim ersten Ballekn der Gruppe das Datum als Header einfügen // (sonst würde es bei jedem Einzelbalken doppelt stehen) return BarTooltipItem( '$formattedDateTime\n$label: ${rod.toY.round()} $unit', const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12, ), ); }, ), ), 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(0, 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(3, 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: 40, // Platz für die normale Textzeile getTitlesWidget: (value, meta) { final index = value.toInt(); if (index < 0 || index >= displayDatetimes.length) { return const SizedBox(); } // 1. Indizes für Start, Mitte und Ende bestimmen final firstIndex = 0; final lastIndex = displayDatetimes.length - 1; final middleIndex = displayDatetimes.length ~/ 2; // 2. Nur anzeigen, wenn es Start, Mitte oder Ende ist final isFirst = index == firstIndex; final isMiddle = index == middleIndex && displayDatetimes.length > 2; final isLast = index == lastIndex && displayDatetimes.length > 1; if (!isFirst && !isMiddle && !isLast) { return const SizedBox(); // Für alle anderen Balken ausblenden } final datetimeRaw = displayDatetimes[index]; // 3. Formatiere den String zu "DD.MM.YY HH:mm" String formattedDateTime = datetimeRaw; try { final parsedDate = DateTime.parse(datetimeRaw); final day = parsedDate.day.toString().padLeft( 2, '0', ); final month = parsedDate.month.toString().padLeft( 2, '0', ); final year = parsedDate.year.toString().substring( 2, ); // Die letzten 2 Stellen final hour = parsedDate.hour.toString().padLeft( 2, '0', ); final minute = parsedDate.minute.toString().padLeft( 2, '0', ); formattedDateTime = '$day.$month.$year $hour:$minute'; } catch (_) { // Fallback, falls String kein echtes ISO-Format ist formattedDateTime = datetimeRaw; } // 4. Liegend (horizontal) ausgeben return Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( formattedDateTime, style: const TextStyle( fontSize: 10, fontWeight: FontWeight.bold, ), ), ); }, ), ), ), 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: 5, // <-- ERHEBLICH SCHMALER (z.B. 2 oder 3 statt 6/8) borderRadius: const BorderRadius.vertical(top: Radius.circular(1)), ), // Roter Bar für Import BarChartRodData( toY: importValue, color: Colors.red, width: 5, // <-- ERHEBLICH SCHMALER borderRadius: const BorderRadius.vertical( top: Radius.circular(2.5), ), ), ], ); }); } } 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; }