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 linéaire professionnel avec animations et interactions class ProfessionalLineChart extends StatefulWidget { const ProfessionalLineChart({ super.key, required this.data, required this.title, this.subtitle, this.showGrid = true, this.showDots = true, this.showArea = false, this.animationDuration = const Duration(milliseconds: 1500), this.lineColor, this.gradientColors, }); final List data; final String title; final String? subtitle; final bool showGrid; final bool showDots; final bool showArea; final Duration animationDuration; final Color? lineColor; final List? gradientColors; @override State createState() => _ProfessionalLineChartState(); } class _ProfessionalLineChartState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation _animation; List _showingTooltipOnSpots = []; @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(), const 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) ...[ const SizedBox(height: DesignSystem.spacingXs), Text( widget.subtitle!, style: DesignSystem.bodyMedium.copyWith( color: AppTheme.textSecondary, ), ), ], ], ); } Widget _buildChart() { return AnimatedBuilder( animation: _animation, builder: (context, child) { return LineChart( LineChartData( lineTouchData: LineTouchData( touchTooltipData: LineTouchTooltipData( tooltipBgColor: AppTheme.textPrimary.withOpacity(0.9), tooltipRoundedRadius: DesignSystem.radiusSm, tooltipPadding: const EdgeInsets.all(DesignSystem.spacingSm), getTooltipItems: (List touchedBarSpots) { return touchedBarSpots.map((barSpot) { final data = widget.data[barSpot.x.toInt()]; return LineTooltipItem( '${data.label}\n${barSpot.y.toInt()}', DesignSystem.labelMedium.copyWith( color: Colors.white, fontWeight: FontWeight.w600, ), ); }).toList(); }, ), handleBuiltInTouches: true, getTouchedSpotIndicator: (LineChartBarData barData, List spotIndexes) { return spotIndexes.map((index) { return TouchedSpotIndicatorData( FlLine( color: widget.lineColor ?? AppTheme.primaryColor, strokeWidth: 2, dashArray: [3, 3], ), FlDotData( getDotPainter: (spot, percent, barData, index) => FlDotCirclePainter( radius: 6, color: widget.lineColor ?? AppTheme.primaryColor, strokeWidth: 2, strokeColor: Colors.white, ), ), ); }).toList(); }, ), gridData: FlGridData( show: widget.showGrid, drawVerticalLine: false, horizontalInterval: _getMaxY() / 5, getDrawingHorizontalLine: (value) { return FlLine( color: AppTheme.borderColor.withOpacity(0.3), strokeWidth: 1, ); }, ), 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), minX: 0, maxX: widget.data.length.toDouble() - 1, minY: 0, maxY: _getMaxY() * 1.2, lineBarsData: [ _buildLineBarData(), ], ), ); }, ); } LineChartBarData _buildLineBarData() { final spots = widget.data.asMap().entries.map((entry) { final index = entry.key; final data = entry.value; return FlSpot(index.toDouble(), data.value * _animation.value); }).toList(); return LineChartBarData( spots: spots, isCurved: true, curveSmoothness: 0.3, color: widget.lineColor ?? AppTheme.primaryColor, barWidth: 3, isStrokeCapRound: true, dotData: FlDotData( show: widget.showDots, getDotPainter: (spot, percent, barData, index) => FlDotCirclePainter( radius: 4, color: widget.lineColor ?? AppTheme.primaryColor, strokeWidth: 2, strokeColor: Colors.white, ), ), belowBarData: widget.showArea ? BarAreaData( show: true, gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: widget.gradientColors ?? [ (widget.lineColor ?? AppTheme.primaryColor).withOpacity(0.3), (widget.lineColor ?? AppTheme.primaryColor).withOpacity(0.05), ], ), ) : BarAreaData(show: false), ); } 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: const 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 linéaire class LineDataPoint { const LineDataPoint({ required this.label, required this.value, }); final String label; final double value; }