import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:geosector_app/presentation/widgets/charts/payment_data.dart'; import 'dart:math' as math; /// Widget de graphique en camembert pour représenter la répartition des règlements class PaymentPieChart extends StatefulWidget { /// Liste des données de règlement à afficher dans le graphique final List payments; /// Taille du graphique final double size; /// Taille des étiquettes final double labelSize; /// Afficher les pourcentages final bool showPercentage; /// Afficher les icônes final bool showIcons; /// Afficher la légende final bool showLegend; /// Format donut (anneau) final bool isDonut; /// Rayon central pour le format donut (en pourcentage) final String innerRadius; /// Activer l'effet 3D final bool enable3DEffect; /// Intensité de l'effet 3D (1.0 = normal, 2.0 = fort) final double effect3DIntensity; /// Activer l'effet d'explosion plus prononcé final bool enableEnhancedExplode; /// Utiliser un dégradé pour simuler l'effet 3D final bool useGradient; const PaymentPieChart({ super.key, required this.payments, this.size = 300, this.labelSize = 12, this.showPercentage = true, this.showIcons = true, this.showLegend = true, this.isDonut = false, this.innerRadius = '40%', this.enable3DEffect = false, this.effect3DIntensity = 1.0, this.enableEnhancedExplode = false, this.useGradient = false, }); @override State createState() => _PaymentPieChartState(); } class _PaymentPieChartState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 2000), ); _animationController.forward(); } @override void didUpdateWidget(PaymentPieChart oldWidget) { super.didUpdateWidget(oldWidget); // Relancer l'animation si les données ont changé // Utiliser une comparaison plus stricte pour éviter des animations inutiles bool shouldResetAnimation = false; if (oldWidget.payments.length != widget.payments.length) { shouldResetAnimation = true; } else { // Comparer les éléments importants uniquement for (int i = 0; i < oldWidget.payments.length; i++) { if (i >= widget.payments.length) break; if (oldWidget.payments[i].amount != widget.payments[i].amount || oldWidget.payments[i].title != widget.payments[i].title) { shouldResetAnimation = true; break; } } } if (shouldResetAnimation) { _animationController.reset(); _animationController.forward(); } } @override void dispose() { _animationController.dispose(); super.dispose(); } /// Prépare les données pour le graphique en camembert List _prepareChartData() { // Filtrer les règlements avec un montant > 0 return widget.payments.where((payment) => payment.amount > 0).toList(); } @override Widget build(BuildContext context) { final chartData = _prepareChartData(); // Si aucune donnée, afficher un message if (chartData.isEmpty) { return SizedBox( width: widget.size, height: widget.size, child: const Center( child: Text('Aucune donnée disponible'), ), ); } // Créer des animations pour différents aspects du graphique final progressAnimation = CurvedAnimation( parent: _animationController, curve: Curves.easeOutCubic, ); final explodeAnimation = CurvedAnimation( parent: _animationController, curve: Interval(0.7, 1.0, curve: Curves.elasticOut), ); final opacityAnimation = CurvedAnimation( parent: _animationController, curve: Interval(0.1, 0.5, curve: Curves.easeIn), ); return AnimatedBuilder( animation: _animationController, builder: (context, child) { return SizedBox( width: widget.size, height: widget.size, child: SfCircularChart( margin: EdgeInsets.zero, legend: Legend( isVisible: widget.showLegend, position: LegendPosition.bottom, overflowMode: LegendItemOverflowMode.wrap, textStyle: TextStyle(fontSize: widget.labelSize), ), tooltipBehavior: TooltipBehavior(enable: true), series: [ widget.isDonut ? DoughnutSeries( dataSource: chartData, xValueMapper: (PaymentData data, _) => data.title, yValueMapper: (PaymentData data, _) => data.amount, pointColorMapper: (PaymentData data, _) { if (widget.enable3DEffect) { // Utiliser un angle différent pour chaque segment pour simuler un effet 3D final index = chartData.indexOf(data); final angle = (index / chartData.length) * 2 * math.pi; return widget.useGradient ? _createEnhanced3DColor(data.color, angle) : _create3DColor( data.color, widget.effect3DIntensity); } return data.color; }, // Note: Le gradient n'est pas directement pris en charge dans cette version de Syncfusion enableTooltip: true, dataLabelMapper: (PaymentData data, _) { if (widget.showPercentage) { // Calculer le pourcentage avec une décimale final total = chartData.fold( 0.0, (sum, item) => sum + item.amount); final percentage = (data.amount / total * 100); return '${percentage.toStringAsFixed(1)}%'; } else { return data.title; } }, dataLabelSettings: DataLabelSettings( isVisible: true, labelPosition: ChartDataLabelPosition .inside, // Afficher les étiquettes à l'intérieur du donut textStyle: TextStyle( fontSize: widget.labelSize, color: Colors .white, // Texte blanc pour meilleure lisibilité fontWeight: FontWeight .bold, // Texte en gras pour meilleure lisibilité ), ), innerRadius: widget.innerRadius, // Effet d'explosion plus prononcé pour donner du relief avec animation explode: true, explodeAll: widget.enableEnhancedExplode, explodeIndex: widget.enableEnhancedExplode ? null : 0, explodeOffset: widget.enableEnhancedExplode ? widget.enable3DEffect ? '${(12 * explodeAnimation.value).toStringAsFixed(1)}%' : '${(8 * explodeAnimation.value).toStringAsFixed(1)}%' : '${(5 * explodeAnimation.value).toStringAsFixed(1)}%', // Effet 3D via l'opacité et les couleurs avec animation opacity: widget.enable3DEffect ? 0.95 * opacityAnimation.value : opacityAnimation.value, // Animation progressive du graphique animationDuration: 0, // On désactive l'animation intégrée car nous utilisons notre propre animation startAngle: 270, endAngle: 270 + (360 * progressAnimation.value).toInt(), ) : PieSeries( dataSource: chartData, xValueMapper: (PaymentData data, _) => data.title, yValueMapper: (PaymentData data, _) => data.amount, pointColorMapper: (PaymentData data, _) { if (widget.enable3DEffect) { // Utiliser un angle différent pour chaque segment pour simuler un effet 3D final index = chartData.indexOf(data); final angle = (index / chartData.length) * 2 * math.pi; return widget.useGradient ? _createEnhanced3DColor(data.color, angle) : _create3DColor( data.color, widget.effect3DIntensity); } return data.color; }, // Note: Le gradient n'est pas directement pris en charge dans cette version de Syncfusion enableTooltip: true, dataLabelMapper: (PaymentData data, _) { if (widget.showPercentage) { // Calculer le pourcentage avec une décimale final total = chartData.fold( 0.0, (sum, item) => sum + item.amount); final percentage = (data.amount / total * 100); return '${percentage.toStringAsFixed(1)}%'; } else { return data.title; } }, dataLabelSettings: DataLabelSettings( isVisible: true, labelPosition: ChartDataLabelPosition.outside, textStyle: TextStyle(fontSize: widget.labelSize), connectorLineSettings: const ConnectorLineSettings( type: ConnectorType.curve, length: '15%', ), ), // Effet d'explosion plus prononcé pour donner du relief avec animation explode: true, explodeAll: widget.enableEnhancedExplode, explodeIndex: widget.enableEnhancedExplode ? null : 0, explodeOffset: widget.enableEnhancedExplode ? widget.enable3DEffect ? '${(12 * explodeAnimation.value).toStringAsFixed(1)}%' : '${(8 * explodeAnimation.value).toStringAsFixed(1)}%' : '${(5 * explodeAnimation.value).toStringAsFixed(1)}%', // Effet 3D via l'opacité et les couleurs avec animation opacity: widget.enable3DEffect ? 0.95 * opacityAnimation.value : opacityAnimation.value, // Animation progressive du graphique animationDuration: 0, // On désactive l'animation intégrée car nous utilisons notre propre animation startAngle: 270, endAngle: 270 + (360 * progressAnimation.value).toInt(), ), ], annotations: widget.showIcons ? _buildIconAnnotations(chartData) : null, // Paramètres pour améliorer l'effet 3D palette: widget.enable3DEffect ? _create3DPalette(chartData) : null, // Ajouter un effet de bordure pour renforcer l'effet 3D borderWidth: widget.enable3DEffect ? 0.5 : 0, // Note: La rotation n'est pas directement prise en charge dans cette version de Syncfusion ), ); }, ); } /// Crée une couleur avec effet 3D en ajoutant des nuances Color _create3DColor(Color baseColor, double intensity) { // Ajuster la luminosité et la saturation pour créer un effet 3D plus prononcé final hslColor = HSLColor.fromColor(baseColor); // Augmenter la luminosité pour simuler un éclairage final adjustedLightness = (hslColor.lightness + 0.15 * intensity).clamp(0.0, 1.0); // Augmenter légèrement la saturation pour des couleurs plus vives final adjustedSaturation = (hslColor.saturation + 0.05 * intensity).clamp(0.0, 1.0); return hslColor .withLightness(adjustedLightness) .withSaturation(adjustedSaturation) .toColor(); } /// Crée une palette de couleurs pour l'effet 3D List _create3DPalette(List chartData) { List palette = []; // Créer des variations de couleurs pour chaque segment for (var i = 0; i < chartData.length; i++) { var data = chartData[i]; // Calculer un angle pour chaque segment pour simuler un éclairage directionnel final angle = (i / chartData.length) * 2 * math.pi; // Créer un effet d'ombre et de lumière en fonction de l'angle final hslColor = HSLColor.fromColor(data.color); // Ajuster la luminosité en fonction de l'angle final lightAdjustment = 0.15 * widget.effect3DIntensity * math.sin(angle); final adjustedLightness = (hslColor.lightness - 0.1 * widget.effect3DIntensity + lightAdjustment) .clamp(0.0, 1.0); // Ajuster la saturation pour plus de profondeur final adjustedSaturation = (hslColor.saturation + 0.1 * widget.effect3DIntensity) .clamp(0.0, 1.0); final enhancedColor = hslColor .withLightness(adjustedLightness) .withSaturation(adjustedSaturation) .toColor(); palette.add(enhancedColor); } return palette; } /// Crée une couleur avec effet 3D plus avancé Color _createEnhanced3DColor(Color baseColor, double angle) { // Simuler un effet de lumière directionnel final hslColor = HSLColor.fromColor(baseColor); // Ajuster la luminosité en fonction de l'angle pour simuler un éclairage final adjustedLightness = hslColor.lightness + (0.2 * widget.effect3DIntensity * math.sin(angle)).clamp(-0.3, 0.3); return hslColor.withLightness(adjustedLightness.clamp(0.0, 1.0)).toColor(); } /// Crée les annotations d'icônes pour le graphique List _buildIconAnnotations( List chartData) { final List annotations = []; // Calculer le total pour les pourcentages double total = chartData.fold(0.0, (sum, item) => sum + item.amount); // Position angulaire actuelle (en radians) double currentAngle = 0; for (int i = 0; i < chartData.length; i++) { final data = chartData[i]; final percentage = data.amount / total; // Calculer l'angle central de ce segment final segmentAngle = percentage * 2 * 3.14159; final midAngle = currentAngle + (segmentAngle / 2); // Ajouter une annotation pour l'icône annotations.add( CircularChartAnnotation( widget: Icon( data.icon, color: Colors.white, size: 16, ), radius: '50%', angle: (midAngle * (180 / 3.14159)).toInt(), // Convertir en degrés ), ); // Mettre à jour l'angle actuel currentAngle += segmentAngle; } return annotations; } }