import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:intl/intl.dart'; import '../../../../shared/theme/app_theme.dart'; import '../../../../shared/theme/design_system.dart'; /// Graphique en barres professionnel avec animations et interactions class ProfessionalBarChart extends StatefulWidget { const ProfessionalBarChart({ super.key, required this.data, required this.title, this.subtitle, this.showGrid = true, this.showValues = true, this.animationDuration = const Duration(milliseconds: 1500), this.barColor, this.gradientColors, }); final List data; final String title; final String? subtitle; final bool showGrid; final bool showValues; final Duration animationDuration; final Color? barColor; final List? gradientColors; @override State createState() => _ProfessionalBarChartState(); } class _ProfessionalBarChartState 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: _buildChart(), ), ], ); } 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 BarChart( BarChartData( alignment: BarChartAlignment.spaceAround, maxY: _getMaxY() * 1.2, barTouchData: BarTouchData( touchTooltipData: BarTouchTooltipData( tooltipBgColor: AppTheme.textPrimary.withOpacity(0.9), tooltipRoundedRadius: DesignSystem.radiusSm, tooltipPadding: EdgeInsets.all(DesignSystem.spacingSm), getTooltipItem: (group, groupIndex, rod, rodIndex) { return BarTooltipItem( '${widget.data[groupIndex].label}\n${rod.toY.toInt()}', DesignSystem.labelMedium.copyWith( color: Colors.white, fontWeight: FontWeight.w600, ), ); }, ), touchCallback: (FlTouchEvent event, barTouchResponse) { setState(() { if (!event.isInterestedForInteractions || barTouchResponse == null || barTouchResponse.spot == null) { _touchedIndex = -1; return; } _touchedIndex = barTouchResponse.spot!.touchedBarGroupIndex; }); }, ), titlesData: FlTitlesData( show: true, rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: _buildBottomTitles, reservedSize: 42, ), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: _buildLeftTitles, reservedSize: 40, ), ), ), borderData: FlBorderData(show: false), gridData: FlGridData( show: widget.showGrid, drawVerticalLine: false, horizontalInterval: _getMaxY() / 5, getDrawingHorizontalLine: (value) { return FlLine( color: AppTheme.borderColor.withOpacity(0.3), strokeWidth: 1, ); }, ), barGroups: _buildBarGroups(), ), ); }, ); } List _buildBarGroups() { return widget.data.asMap().entries.map((entry) { final index = entry.key; final data = entry.value; final isTouched = index == _touchedIndex; return BarChartGroupData( x: index, barRods: [ BarChartRodData( toY: data.value * _animation.value, color: _getBarColor(index, isTouched), width: isTouched ? 24 : 20, borderRadius: BorderRadius.only( topLeft: Radius.circular(DesignSystem.radiusXs), topRight: Radius.circular(DesignSystem.radiusXs), ), gradient: widget.gradientColors != null ? LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, colors: widget.gradientColors!, ) : null, ), ], showingTooltipIndicators: isTouched ? [0] : [], ); }).toList(); } Color _getBarColor(int index, bool isTouched) { if (widget.barColor != null) { return isTouched ? widget.barColor! : widget.barColor!.withOpacity(0.8); } final colors = DesignSystem.chartColors; final color = colors[index % colors.length]; return isTouched ? color : color.withOpacity(0.8); } Widget _buildBottomTitles(double value, TitleMeta meta) { if (value.toInt() >= widget.data.length) return const SizedBox.shrink(); final data = widget.data[value.toInt()]; return SideTitleWidget( axisSide: meta.axisSide, child: Padding( padding: EdgeInsets.only(top: DesignSystem.spacingXs), child: Text( data.label, style: DesignSystem.labelSmall.copyWith( color: AppTheme.textSecondary, fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, ), ), ); } Widget _buildLeftTitles(double value, TitleMeta meta) { return SideTitleWidget( axisSide: meta.axisSide, child: Text( value.toInt().toString(), style: DesignSystem.labelSmall.copyWith( color: AppTheme.textSecondary, ), ), ); } double _getMaxY() { if (widget.data.isEmpty) return 10; return widget.data.map((e) => e.value).reduce((a, b) => a > b ? a : b); } } /// Modèle de données pour le graphique en barres class BarDataPoint { const BarDataPoint({ required this.label, required this.value, this.color, }); final String label; final double value; final Color? color; }