import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales import 'package:flutter/material.dart'; import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales import 'package:flutter/foundation.dart' show listEquals, mapEquals; import 'package:intl/intl.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:geosector_app/core/repositories/passage_repository.dart'; import 'package:geosector_app/core/repositories/user_repository.dart'; import 'package:geosector_app/core/services/passage_data_service.dart'; /// Modèle de données pour le graphique en camembert des passages class PassageChartData { /// Identifiant du type de passage final int typeId; /// Nombre de passages de ce type final int count; /// Titre du type de passage final String title; /// Couleur associée au type de passage final Color color; /// Icône associée au type de passage final IconData icon; PassageChartData({ required this.typeId, required this.count, required this.title, required this.color, required this.icon, }); } /// Widget de graphique en camembert pour représenter la répartition des passages par type class PassagePieChart extends StatefulWidget { /// Liste des données de passages par type sous forme de Map avec typeId et count /// Si loadFromHive est true, ce paramètre est ignoré final Map passagesByType; /// 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; /// Charger les données depuis Hive final bool loadFromHive; /// ID de l'utilisateur pour filtrer les passages (utilisé seulement si loadFromHive est true) final int? userId; /// Types de passages à exclure (utilisé seulement si loadFromHive est true) final List excludePassageTypes; /// Afficher tous les passages sans filtrer par utilisateur (utilisé seulement si loadFromHive est true) final bool showAllPassages; const PassagePieChart({ super.key, this.passagesByType = const {}, this.size = 300, this.labelSize = 12, this.showPercentage = true, this.showIcons = true, this.showLegend = true, this.isDonut = false, this.innerRadius = '40%', this.loadFromHive = false, this.userId, this.excludePassageTypes = const [2], this.showAllPassages = false, }); @override State createState() => _PassagePieChartState(); } class _PassagePieChartState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; /// Données de passages par type late Map _passagesByType; /// Variables pour la mise en cache et l'optimisation bool _dataLoaded = false; bool _isLoading = false; List? _cachedChartData; List? _cachedAnnotations; @override void initState() { super.initState(); _passagesByType = widget.passagesByType; // Initialiser le contrôleur d'animation _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 2000), ); _animationController.forward(); // Si nous n'utilisons pas Hive, préparer les données immédiatement if (!widget.loadFromHive) { _prepareChartData(); } } @override void didChangeDependencies() { super.didChangeDependencies(); if (widget.loadFromHive && !_dataLoaded && !_isLoading) { _isLoading = true; // Prévenir les chargements multiples _loadPassageDataFromHive(context); } } @override void didUpdateWidget(PassagePieChart oldWidget) { super.didUpdateWidget(oldWidget); // Vérifier si les propriétés importantes ont changé final bool dataSourceChanged = widget.loadFromHive ? false : !mapEquals(oldWidget.passagesByType, widget.passagesByType); final bool filteringChanged = oldWidget.userId != widget.userId || !listEquals( oldWidget.excludePassageTypes, widget.excludePassageTypes) || oldWidget.showAllPassages != widget.showAllPassages; final bool visualChanged = oldWidget.size != widget.size || oldWidget.labelSize != widget.labelSize || oldWidget.showPercentage != widget.showPercentage || oldWidget.showIcons != widget.showIcons || oldWidget.showLegend != widget.showLegend || oldWidget.isDonut != widget.isDonut || oldWidget.innerRadius != widget.innerRadius; // Si les paramètres de filtrage ou de source de données ont changé, recharger les données if (dataSourceChanged || filteringChanged) { _cachedChartData = null; _cachedAnnotations = null; // Relancer l'animation si les données ont changé _animationController.reset(); _animationController.forward(); if (!widget.loadFromHive) { _passagesByType = widget.passagesByType; _prepareChartData(); } else if (!_isLoading) { _dataLoaded = false; _isLoading = true; _loadPassageDataFromHive(context); } } // Si seuls les paramètres visuels ont changé, recalculer les annotations sans recharger les données else if (visualChanged) { _cachedAnnotations = null; } } /// Charge les données de passage depuis Hive en utilisant le service PassageDataService void _loadPassageDataFromHive(BuildContext context) { // Éviter les appels multiples pendant le chargement if (_isLoading) { debugPrint('PassagePieChart: Déjà en cours de chargement, ignoré'); return; } // Si les données sont déjà chargées et non vides, ne pas recharger if (_dataLoaded && _passagesByType.isNotEmpty) { debugPrint('PassagePieChart: Données déjà chargées, ignoré'); return; } debugPrint('PassagePieChart: Début du chargement des données'); setState(() { _isLoading = true; }); // Charger les données dans un addPostFrameCallback pour éviter les problèmes de cycle de vie WidgetsBinding.instance.addPostFrameCallback((_) { // Vérifier si le widget est toujours monté if (!mounted) { debugPrint('PassagePieChart: Widget démonté, chargement annulé'); return; } try { debugPrint('PassagePieChart: Création du service de données'); // Utiliser les instances globales définies dans app.dart // Vérifier que les repositories sont disponibles if (passageRepository == null) { debugPrint('PassagePieChart: ERREUR - passageRepository est null'); if (mounted) { setState(() { _isLoading = false; }); } return; } if (userRepository == null) { debugPrint('PassagePieChart: ERREUR - userRepository est null'); if (mounted) { setState(() { _isLoading = false; }); } return; } // Créer une instance du service de données final passageDataService = PassageDataService( passageRepository: passageRepository, userRepository: userRepository, ); debugPrint( 'PassagePieChart: Chargement des données avec excludePassageTypes=${widget.excludePassageTypes}, userId=${widget.userId}, showAllPassages=${widget.showAllPassages}'); // Utiliser le service pour charger les données final data = passageDataService.loadPassageDataForPieChart( excludePassageTypes: widget.excludePassageTypes, userId: widget.userId, showAllPassages: widget.showAllPassages, ); debugPrint('PassagePieChart: Données chargées: $data'); // Mettre à jour les données et les états if (mounted) { setState(() { _passagesByType = data; _dataLoaded = true; _isLoading = false; _cachedChartData = null; // Forcer la régénération des données du graphique _cachedAnnotations = null; }); // Préparer les données du graphique _prepareChartData(); debugPrint('PassagePieChart: Données préparées pour le graphique'); } } catch (e) { // Gérer les erreurs et réinitialiser l'état pour permettre une future tentative debugPrint( 'PassagePieChart: ERREUR lors du chargement des données: $e'); if (mounted) { setState(() { _isLoading = false; }); } } }); } /// Prépare les données pour le graphique en camembert avec mise en cache List _prepareChartData() { // Utiliser les données en cache si disponibles if (_cachedChartData != null) { debugPrint('PassagePieChart: Utilisation des données en cache'); return _cachedChartData!; } debugPrint('PassagePieChart: Préparation des données pour le graphique'); debugPrint('PassagePieChart: Données brutes: $_passagesByType'); // Vérifier si les données sont vides if (_passagesByType.isEmpty) { debugPrint('PassagePieChart: Aucune donnée disponible'); return []; } // Vérifier si les données contiennent uniquement des passages de type 2 bool onlyType2 = true; _passagesByType.forEach((typeId, count) { if (typeId != 2 && count > 0) { onlyType2 = false; } }); if (onlyType2) { debugPrint( 'PassagePieChart: Les données contiennent uniquement des passages de type 2'); } final List chartData = []; // Créer les données du graphique _passagesByType.forEach((typeId, count) { // Vérifier que le type existe et que le compteur est positif if (count > 0 && AppKeys.typesPassages.containsKey(typeId)) { // Vérifier si le type est exclu bool isExcluded = widget.excludePassageTypes.contains(typeId); if (isExcluded) { debugPrint('PassagePieChart: Type $typeId exclu'); } else { final typeInfo = AppKeys.typesPassages[typeId]!; final typeName = typeInfo['titre'] as String; debugPrint( 'PassagePieChart: Ajout du type $typeId ($typeName) avec $count passages'); chartData.add(PassageChartData( typeId: typeId, count: count, title: typeName, color: Color(typeInfo['couleur2'] as int), icon: typeInfo['icon_data'] as IconData, )); } } else { if (count <= 0) { debugPrint('PassagePieChart: Type $typeId ignoré car count=$count'); } else if (!AppKeys.typesPassages.containsKey(typeId)) { debugPrint( 'PassagePieChart: Type $typeId ignoré car non défini dans AppKeys.typesPassages'); } } }); debugPrint( 'PassagePieChart: ${chartData.length} types de passages ajoutés au graphique'); // Mettre en cache les données générées _cachedChartData = chartData; return chartData; } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { // Si les données doivent être chargées depuis Hive mais ne sont pas encore prêtes if (widget.loadFromHive && !_dataLoaded) { return SizedBox( width: widget.size, height: widget.size, child: const Center( child: CircularProgressIndicator(), ), ); } 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: (PassageChartData data, _) => data.title, yValueMapper: (PassageChartData data, _) => data.count, pointColorMapper: (PassageChartData data, _) => data.color, enableTooltip: true, dataLabelMapper: (PassageChartData data, _) { if (widget.showPercentage) { // Calculer le pourcentage avec une décimale final total = chartData.fold( 0, (sum, item) => sum + item.count); final percentage = (data.count / 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%', ), ), innerRadius: widget.innerRadius, explode: true, explodeIndex: 0, explodeOffset: '${(5 * explodeAnimation.value).toStringAsFixed(1)}%', opacity: opacityAnimation.value, 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: (PassageChartData data, _) => data.title, yValueMapper: (PassageChartData data, _) => data.count, pointColorMapper: (PassageChartData data, _) => data.color, enableTooltip: true, dataLabelMapper: (PassageChartData data, _) { if (widget.showPercentage) { // Calculer le pourcentage avec une décimale final total = chartData.fold( 0, (sum, item) => sum + item.count); final percentage = (data.count / 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%', ), ), explode: true, explodeIndex: 0, explodeOffset: '${(5 * explodeAnimation.value).toStringAsFixed(1)}%', opacity: opacityAnimation.value, 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, ), ); }, ); } /// Crée les annotations d'icônes pour le graphique avec mise en cache List _buildIconAnnotations( List chartData) { // Utiliser les annotations en cache si disponibles if (_cachedAnnotations != null) { return _cachedAnnotations!; } final List annotations = []; // Calculer le total pour les pourcentages int total = chartData.fold(0, (sum, item) => sum + item.count); if (total == 0) return []; // Éviter la division par zéro // Position angulaire actuelle (en radians) double currentAngle = 0; for (int i = 0; i < chartData.length; i++) { final data = chartData[i]; final percentage = data.count / 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; } // Mettre en cache les annotations générées _cachedAnnotations = annotations; return annotations; } }