/// Graphique d'évolution des scores. /// /// Affiche un graphique linéaire montrant l'évolution des scores /// sur les 10 dernières sessions. Utilise fl_chart pour le rendu. library; import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:intl/intl.dart'; import '../../../core/constants/app_constants.dart'; import '../../../core/theme/app_theme.dart'; import '../../../data/models/session.dart'; class HistoryChart extends StatelessWidget { final List sessions; const HistoryChart({ super.key, required this.sessions, }); @override Widget build(BuildContext context) { if (sessions.length < 2) { return const SizedBox.shrink(); } // Sort sessions by date and take last 10 final sortedSessions = List.from(sessions) ..sort((a, b) => a.createdAt.compareTo(b.createdAt)); final displaySessions = sortedSessions.length > 10 ? sortedSessions.sublist(sortedSessions.length - 10) : sortedSessions; return Card( child: Padding( padding: const EdgeInsets.all(AppConstants.defaultPadding), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.show_chart, color: AppTheme.primaryColor), const SizedBox(width: 8), Text( 'Evolution des scores', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 16), SizedBox( height: 200, child: LineChart( LineChartData( gridData: FlGridData( show: true, drawVerticalLine: false, horizontalInterval: 20, getDrawingHorizontalLine: (value) { return FlLine( color: Colors.grey[300], strokeWidth: 1, ); }, ), titlesData: FlTitlesData( show: true, bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 30, interval: 1, getTitlesWidget: (value, meta) { final index = value.toInt(); if (index < 0 || index >= displaySessions.length) { return const SizedBox.shrink(); } return Padding( padding: const EdgeInsets.only(top: 8), child: Text( DateFormat('dd/MM').format(displaySessions[index].createdAt), style: const TextStyle(fontSize: 10), ), ); }, ), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 40, interval: 20, getTitlesWidget: (value, meta) { return Text( value.toInt().toString(), style: const TextStyle(fontSize: 10), ); }, ), ), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), ), borderData: FlBorderData( show: true, border: Border( bottom: BorderSide(color: Colors.grey[300]!), left: BorderSide(color: Colors.grey[300]!), ), ), minX: 0, maxX: (displaySessions.length - 1).toDouble(), minY: 0, maxY: _getMaxY(displaySessions), lineBarsData: [ // Score line LineChartBarData( spots: displaySessions.asMap().entries.map((entry) { return FlSpot( entry.key.toDouble(), entry.value.totalScore.toDouble(), ); }).toList(), isCurved: true, color: AppTheme.primaryColor, barWidth: 3, isStrokeCapRound: true, dotData: FlDotData( show: true, getDotPainter: (spot, percent, barData, index) { return FlDotCirclePainter( radius: 4, color: AppTheme.primaryColor, strokeWidth: 2, strokeColor: Colors.white, ); }, ), belowBarData: BarAreaData( show: true, color: AppTheme.primaryColor.withValues(alpha: 0.1), ), ), ], lineTouchData: LineTouchData( touchTooltipData: LineTouchTooltipData( getTooltipItems: (touchedSpots) { return touchedSpots.map((spot) { final session = displaySessions[spot.x.toInt()]; return LineTooltipItem( 'Score: ${session.totalScore}\n${session.shotCount} tirs', const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), ); }).toList(); }, ), ), ), ), ), const SizedBox(height: 8), _buildLegend(context, displaySessions), ], ), ), ); } double _getMaxY(List sessions) { double maxScore = 0; for (final session in sessions) { if (session.totalScore > maxScore) { maxScore = session.totalScore.toDouble(); } } return (maxScore * 1.2).ceilToDouble(); } Widget _buildLegend(BuildContext context, List displaySessions) { final avgScore = displaySessions.fold(0, (sum, s) => sum + s.totalScore) / displaySessions.length; final trend = displaySessions.length >= 2 ? displaySessions.last.totalScore - displaySessions.first.totalScore : 0; return Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildLegendItem( context, 'Moyenne', avgScore.toStringAsFixed(1), Icons.analytics, AppTheme.primaryColor, ), _buildLegendItem( context, 'Tendance', trend >= 0 ? '+$trend' : '$trend', trend >= 0 ? Icons.trending_up : Icons.trending_down, trend >= 0 ? AppTheme.successColor : AppTheme.errorColor, ), _buildLegendItem( context, 'Sessions', '${displaySessions.length}', Icons.list, Colors.grey, ), ], ); } Widget _buildLegendItem( BuildContext context, String label, String value, IconData icon, Color color, ) { return Column( children: [ Icon(icon, size: 16, color: color), const SizedBox(height: 4), Text( value, style: TextStyle( fontWeight: FontWeight.bold, color: color, ), ), Text( label, style: Theme.of(context).textTheme.bodySmall, ), ], ); } }