import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import '../../../../shared/theme/app_theme.dart'; import '../../../../shared/theme/design_system.dart'; /// Graphique en secteurs professionnel avec animations et légendes class ProfessionalPieChart extends StatefulWidget { const ProfessionalPieChart({ super.key, required this.data, required this.title, this.subtitle, this.centerText, this.showLegend = true, this.showPercentages = true, this.animationDuration = const Duration(milliseconds: 1500), }); final List data; final String title; final String? subtitle; final String? centerText; final bool showLegend; final bool showPercentages; final Duration animationDuration; @override State createState() => _ProfessionalPieChartState(); } class _ProfessionalPieChartState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation _animation; int _touchedIndex = -1; @override void initState() { super.initState(); _animationController = AnimationController( duration: widget.animationDuration, vsync: this, ); _animation = Tween( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _animationController, curve: DesignSystem.animationCurve, )); _animationController.forward(); } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHeader(), SizedBox(height: DesignSystem.spacingLg), Expanded( child: Row( children: [ Expanded( flex: 3, child: _buildChart(), ), if (widget.showLegend) ...[ SizedBox(width: DesignSystem.spacingLg), Expanded( flex: 2, child: _buildLegend(), ), ], ], ), ), ], ); } Widget _buildHeader() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.title, style: DesignSystem.titleLarge.copyWith( fontWeight: FontWeight.w700, ), ), if (widget.subtitle != null) ...[ SizedBox(height: DesignSystem.spacingXs), Text( widget.subtitle!, style: DesignSystem.bodyMedium.copyWith( color: AppTheme.textSecondary, ), ), ], ], ); } Widget _buildChart() { return AnimatedBuilder( animation: _animation, builder: (context, child) { return Container( height: 140, // Hauteur encore plus réduite padding: const EdgeInsets.all(4), // Padding minimal pour contenir le graphique child: PieChart( PieChartData( pieTouchData: PieTouchData( touchCallback: (FlTouchEvent event, pieTouchResponse) { setState(() { if (!event.isInterestedForInteractions || pieTouchResponse == null || pieTouchResponse.touchedSection == null) { _touchedIndex = -1; return; } _touchedIndex = pieTouchResponse.touchedSection!.touchedSectionIndex; }); }, ), borderData: FlBorderData(show: false), sectionsSpace: 1, // Espace réduit entre sections centerSpaceRadius: widget.centerText != null ? 45 : 30, // Rayon central réduit sections: _buildSections(), ), ), ); }, ); } List _buildSections() { final total = widget.data.fold(0, (sum, item) => sum + item.value); return widget.data.asMap().entries.map((entry) { final index = entry.key; final data = entry.value; final isTouched = index == _touchedIndex; final percentage = (data.value / total * 100); return PieChartSectionData( color: data.color, value: data.value * _animation.value, title: widget.showPercentages ? '${percentage.toStringAsFixed(1)}%' : '', radius: isTouched ? 70 : 60, titleStyle: DesignSystem.labelMedium.copyWith( color: Colors.white, fontWeight: FontWeight.w600, shadows: [ Shadow( color: Colors.black.withOpacity(0.3), offset: const Offset(1, 1), blurRadius: 2, ), ], ), titlePositionPercentageOffset: 0.6, badgeWidget: isTouched ? _buildBadge(data) : null, badgePositionPercentageOffset: 1.3, ); }).toList(); } Widget _buildBadge(ChartDataPoint data) { return Container( padding: EdgeInsets.symmetric( horizontal: DesignSystem.spacingSm, vertical: DesignSystem.spacingXs, ), decoration: BoxDecoration( color: data.color, borderRadius: BorderRadius.circular(DesignSystem.radiusSm), boxShadow: DesignSystem.shadowCard, ), child: Text( data.value.toInt().toString(), style: DesignSystem.labelMedium.copyWith( color: Colors.white, fontWeight: FontWeight.w600, ), ), ); } Widget _buildLegend() { return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ if (widget.centerText != null) ...[ _buildCenterInfo(), SizedBox(height: DesignSystem.spacingLg), ], ...widget.data.asMap().entries.map((entry) { final index = entry.key; final data = entry.value; final isSelected = index == _touchedIndex; return AnimatedContainer( duration: DesignSystem.animationFast, margin: EdgeInsets.only(bottom: DesignSystem.spacingSm), padding: EdgeInsets.all(DesignSystem.spacingSm), decoration: BoxDecoration( color: isSelected ? data.color.withOpacity(0.1) : Colors.transparent, borderRadius: BorderRadius.circular(DesignSystem.radiusSm), border: isSelected ? Border.all( color: data.color.withOpacity(0.3), width: 1, ) : null, ), child: Row( children: [ Container( width: 16, height: 16, decoration: BoxDecoration( color: data.color, borderRadius: BorderRadius.circular(DesignSystem.radiusXs), ), ), SizedBox(width: DesignSystem.spacingSm), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( data.label, style: DesignSystem.labelLarge.copyWith( fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, ), ), Text( data.value.toInt().toString(), style: DesignSystem.labelMedium.copyWith( color: AppTheme.textSecondary, ), ), ], ), ), ], ), ); }).toList(), ], ); } Widget _buildCenterInfo() { return Container( padding: EdgeInsets.all(DesignSystem.spacingMd), decoration: BoxDecoration( color: AppTheme.primaryColor.withOpacity(0.1), borderRadius: BorderRadius.circular(DesignSystem.radiusMd), border: Border.all( color: AppTheme.primaryColor.withOpacity(0.2), width: 1, ), ), child: Column( children: [ Text( 'Total', style: DesignSystem.labelMedium.copyWith( color: AppTheme.textSecondary, ), ), SizedBox(height: DesignSystem.spacingXs), Text( widget.centerText!, style: DesignSystem.headlineMedium.copyWith( color: AppTheme.primaryColor, fontWeight: FontWeight.w700, ), ), ], ), ); } } /// Modèle de données pour le graphique en secteurs class ChartDataPoint { const ChartDataPoint({ required this.label, required this.value, required this.color, }); final String label; final double value; final Color color; }