From db6dd0cee5ff08b0c7539f64dcc57082dce4d98b Mon Sep 17 00:00:00 2001 From: qguillaume Date: Wed, 6 May 2026 22:39:30 +0200 Subject: [PATCH] feat: refonte des statistiques (00C) et correction ergonomique de la capture (10C) --- lib/features/capture/capture_screen.dart | 54 +- .../statistics/statistics_screen.dart | 894 +++++------------- pubspec.lock | 4 +- pubspec.yaml | 2 +- 4 files changed, 264 insertions(+), 690 deletions(-) diff --git a/lib/features/capture/capture_screen.dart b/lib/features/capture/capture_screen.dart index 5a53be8a..b690b15a 100644 --- a/lib/features/capture/capture_screen.dart +++ b/lib/features/capture/capture_screen.dart @@ -20,11 +20,11 @@ class CaptureScreen extends StatefulWidget { class _CaptureScreenState extends State { final ImagePicker _picker = ImagePicker(); - TargetType _selectedType = TargetType.concentric; + final TargetType _selectedType = TargetType.concentric; String? _selectedImagePath; bool _isLoading = false; - /// Gère la demande de permission et la sélection d'image + /// Gère la demande de permission et la sélection d'image depuis la galerie Future _handleGallerySelection() async { PermissionStatus status; @@ -32,7 +32,6 @@ class _CaptureScreenState extends State { final deviceInfo = DeviceInfoPlugin(); final androidInfo = await deviceInfo.androidInfo; - // Android 13+ (SDK 33) utilise .photos, les versions précédentes utilisent .storage if (androidInfo.version.sdkInt >= 33) { status = await Permission.photos.request(); } else { @@ -106,9 +105,7 @@ class _CaptureScreenState extends State { child: ImageSourceButton( icon: Icons.photo_library, label: 'Galerie', - onPressed: _isLoading - ? null - : _handleGallerySelection, // Appelle la nouvelle fonction + onPressed: _isLoading ? null : _handleGallerySelection, ), ), ], @@ -126,21 +123,44 @@ class _CaptureScreenState extends State { _buildImagePreview() else _buildGuide(), + + // --- NOUVEAU BOUTON VALIDER --- + if (_selectedImagePath != null && !_isLoading) ...[ + const SizedBox(height: 24), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF112233), // Ton bleu foncé + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 54), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + onPressed: _analyzeImage, + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Valider', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(width: 12), + Icon(Icons.arrow_forward, size: 20), + ], + ), + ), + const SizedBox(height: 32), + ], ], ), ), - floatingActionButton: _selectedImagePath != null - ? FloatingActionButton.extended( - onPressed: _analyzeImage, - icon: const Icon(Icons.arrow_forward), - label: const Text('Suivant'), - ) - : null, ); } - // Tes widgets _buildSectionTitle, _buildImagePreview, _buildFramingHints restent identiques - Widget _buildSectionTitle(String title) { return Text( title, @@ -230,7 +250,7 @@ class _CaptureScreenState extends State { _buildGuideItem(Icons.blur_off, 'Évitez les images floues'), _buildGuideItem( Icons.cleaning_services, - 'Nettoyer votre objectif avec une chiffonnette pour lunette si nécessaire', + 'Nettoyer votre objectif avec une chiffonnette', ), ], ), @@ -251,8 +271,6 @@ class _CaptureScreenState extends State { ); } - // --- LOGIQUE DE CAPTURE --- - Future _scanDocument() async { setState(() => _isLoading = true); try { diff --git a/lib/features/statistics/statistics_screen.dart b/lib/features/statistics/statistics_screen.dart index f77edc1d..2738dfcc 100644 --- a/lib/features/statistics/statistics_screen.dart +++ b/lib/features/statistics/statistics_screen.dart @@ -1,21 +1,15 @@ -/// Écran des statistiques détaillées. -/// -/// Affiche les métriques de performance avec filtrage par période -/// (session, semaine, mois, tout). Inclut heat map, précision, -/// écart-type et distribution régionale des tirs. -library; - +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:fl_chart/fl_chart.dart'; // La librairie pour les courbes import '../../core/constants/app_constants.dart'; import '../../core/theme/app_theme.dart'; import '../../data/models/session.dart'; import '../../data/repositories/session_repository.dart'; import '../../services/statistics_service.dart'; -import 'widgets/heat_map_widget.dart'; class StatisticsScreen extends StatefulWidget { - final Session? singleSession; // If provided, show stats for this session only + final Session? singleSession; const StatisticsScreen({super.key, this.singleSession}); @@ -30,720 +24,282 @@ class _StatisticsScreenState extends State { bool _isLoading = true; List _allSessions = []; + // Valeurs pour les Dropdowns (Filtres) + String _selectedWeapon = 'Toutes'; + String _selectedDistance = 'Toutes'; + @override void initState() { super.initState(); - // Use addPostFrameCallback to ensure context is available - WidgetsBinding.instance.addPostFrameCallback((_) { - _loadStatistics(); - }); + WidgetsBinding.instance.addPostFrameCallback((_) => _loadStatistics()); } Future _loadStatistics() async { if (!mounted) return; setState(() => _isLoading = true); - try { - if (widget.singleSession != null) { - // Single session mode - _statistics = _statisticsService.calculateStatistics( - [widget.singleSession!], - period: StatsPeriod.session, - targetCenterX: widget.singleSession!.targetCenterX ?? 0.5, - targetCenterY: widget.singleSession!.targetCenterY ?? 0.5, - ); - } else { - // Load all sessions - final repository = context.read(); - _allSessions = await repository.getAllSessions(); - _calculateStats(); - } + final repository = context.read(); + _allSessions = await repository.getAllSessions(); + _calculateStats(); } catch (e) { - debugPrint('Error loading statistics: $e'); - } - - if (mounted) { - setState(() => _isLoading = false); + debugPrint('Error: $e'); + } finally { + if (mounted) setState(() => _isLoading = false); } } void _calculateStats() { - debugPrint( - 'Calculating stats for ${_allSessions.length} sessions, period: $_selectedPeriod', - ); - for (final session in _allSessions) { - debugPrint( - ' Session: ${session.id}, shots: ${session.shots.length}, date: ${session.createdAt}', - ); - } _statistics = _statisticsService.calculateStatistics( _allSessions, period: _selectedPeriod, ); - debugPrint( - 'Statistics result: totalShots=${_statistics?.totalShots}, totalScore=${_statistics?.totalScore}', - ); } @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: const Color( + 0xFF121212, + ), // Fond sombre comme sur le design appBar: AppBar( - title: Text( - widget.singleSession != null - ? 'Statistiques Session' - : 'Statistiques', + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.pop(context), ), + title: const Text( + 'Statistiques', + style: TextStyle(color: Colors.white), + ), + centerTitle: true, ), body: _isLoading ? const Center(child: CircularProgressIndicator()) - : _statistics == null || _statistics!.totalShots == 0 - ? _buildEmptyState() - : _buildStatistics(), - ); - } - - Widget _buildEmptyState() { - return Center( - child: Padding( - padding: const EdgeInsets.all(AppConstants.defaultPadding), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.analytics_outlined, - size: 64, - color: Colors.grey.shade400, - ), - const SizedBox(height: 16), - Text( - 'Aucune donnee disponible', - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 8), - Text( - 'Effectuez des sessions de tir pour voir vos statistiques', - textAlign: TextAlign.center, - style: TextStyle(color: Colors.grey.shade600), - ), - const SizedBox(height: 16), - Text( - 'Sessions trouvees: ${_allSessions.length}', - style: TextStyle(color: Colors.grey.shade400, fontSize: 12), - ), - if (_allSessions.isNotEmpty) - Text( - 'Tirs totaux: ${_allSessions.fold(0, (sum, s) => sum + s.shots.length)}', - style: TextStyle(color: Colors.grey.shade400, fontSize: 12), - ), - ], - ), - ), - ); - } - - Widget _buildStatistics() { - return RefreshIndicator( - onRefresh: _loadStatistics, - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.all(AppConstants.defaultPadding), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Period filter (only for multi-session view) - if (widget.singleSession == null) _buildPeriodFilter(), - const SizedBox(height: 16), - - // Summary cards - _buildSummaryCards(), - const SizedBox(height: 24), - - // Heat Map - _buildHeatMapSection(), - const SizedBox(height: 24), - - // Precision stats - _buildPrecisionSection(), - const SizedBox(height: 24), - - // Standard deviation - _buildStdDevSection(), - const SizedBox(height: 24), - - // Regional distribution - _buildRegionalSection(), - ], - ), - ), - ); - } - - Widget _buildPeriodFilter() { - return Card( - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Periode', - style: TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - SegmentedButton( - segments: const [ - ButtonSegment( - value: StatsPeriod.week, - label: Text('7 jours'), - icon: Icon(Icons.date_range), - ), - ButtonSegment( - value: StatsPeriod.month, - label: Text('30 jours'), - icon: Icon(Icons.calendar_month), - ), - ButtonSegment( - value: StatsPeriod.all, - label: Text('Tout'), - icon: Icon(Icons.all_inclusive), - ), - ], - selected: {_selectedPeriod}, - onSelectionChanged: (selection) { - setState(() { - _selectedPeriod = selection.first; - _calculateStats(); - }); - }, - ), - const SizedBox(height: 8), - Text( - '${_statistics!.sessions.length} session(s) - ${_statistics!.totalShots} tir(s)', - style: TextStyle(color: Colors.grey.shade600, fontSize: 12), - ), - ], - ), - ), - ); - } - - Widget _buildSummaryCards() { - return Row( - children: [ - Expanded( - child: _StatCard( - icon: Icons.gps_fixed, - title: 'Tirs', - value: '${_statistics!.totalShots}', - color: AppTheme.primaryColor, - ), - ), - const SizedBox(width: 12), - Expanded( - child: _StatCard( - icon: Icons.score, - title: 'Score Total', - value: '${_statistics!.totalScore}', - color: AppTheme.secondaryColor, - ), - ), - ], - ); - } - - Widget _buildHeatMapSection() { - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Icon(Icons.grid_on, color: AppTheme.primaryColor), - const SizedBox(width: 8), - const Text( - 'Zones Chaudes', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), - ], - ), - const SizedBox(height: 8), - Text( - 'Repartition de vos tirs sur la cible', - style: TextStyle(color: Colors.grey.shade600, fontSize: 12), - ), - const SizedBox(height: 16), - Center( - child: HeatMapWidget( - heatMap: _statistics!.heatMap, - size: MediaQuery.of(context).size.width - 80, - ), - ), - const SizedBox(height: 12), - // Legend - gradient bar - Container( - height: 24, - margin: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - gradient: const LinearGradient( - colors: [ - Color(0xFF2196F3), // Blue (cold) - Color(0xFF00BCD4), // Cyan - Color(0xFFFFEB3B), // Yellow - Color(0xFFFF9800), // Orange - Color(0xFFFF1744), // Red (hot) - ], - ), - ), - ), - const SizedBox(height: 4), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(left: 16), - child: Text( - 'Peu', - style: TextStyle(fontSize: 12, color: Colors.grey.shade600), - ), - ), - Padding( - padding: const EdgeInsets.only(right: 16), - child: Text( - 'Beaucoup', - style: TextStyle(fontSize: 12, color: Colors.grey.shade600), - ), - ), - ], - ), - ], - ), - ), - ); - } - - Widget _buildPrecisionSection() { - final precision = _statistics!.precision; - - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Icon( - Icons.center_focus_strong, - color: AppTheme.successColor, - ), - const SizedBox(width: 8), - const Text( - 'Precision', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: _buildPrecisionGauge( - 'Precision', - precision.precisionScore, - 'Distance moyenne du centre', - ), - ), - const SizedBox(width: 16), - Expanded( - child: _buildPrecisionGauge( - 'Regularite', - precision.consistencyScore, - 'Groupement des tirs', - ), - ), - ], - ), - const Divider(height: 32), - _buildStatRow( - 'Distance moyenne du centre', - '${(precision.avgDistanceFromCenter * 100).toStringAsFixed(1)}%', - ), - _buildStatRow( - 'Diametre de groupement', - '${(precision.groupingDiameter * 100).toStringAsFixed(1)}%', - ), - _buildStatRow( - 'Score moyen', - _statistics!.avgScore.toStringAsFixed(2), - ), - _buildStatRow('Meilleur score', '${_statistics!.maxScore}'), - _buildStatRow('Plus bas score', '${_statistics!.minScore}'), - ], - ), - ), - ); - } - - Widget _buildPrecisionGauge(String title, double value, String subtitle) { - final color = value > 70 - ? AppTheme.successColor - : value > 40 - ? AppTheme.warningColor - : AppTheme.errorColor; - - return Column( - children: [ - Stack( - alignment: Alignment.center, - children: [ - SizedBox( - width: 80, - height: 80, - child: CircularProgressIndicator( - value: value / 100, - strokeWidth: 8, - backgroundColor: Colors.grey.shade200, - valueColor: AlwaysStoppedAnimation(color), - ), - ), - Text( - value.toStringAsFixed(0), - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: color, - ), - ), - ], - ), - const SizedBox(height: 8), - Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), - Text( - subtitle, - style: TextStyle(fontSize: 10, color: Colors.grey.shade600), - textAlign: TextAlign.center, - ), - ], - ); - } - - Widget _buildStdDevSection() { - final stdDev = _statistics!.stdDev; - - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Icon( - Icons.stacked_line_chart, - color: AppTheme.warningColor, - ), - const SizedBox(width: 8), - const Text( - 'Ecart Type', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), - ], - ), - const SizedBox(height: 8), - Text( - 'Mesure de la dispersion de vos tirs', - style: TextStyle(color: Colors.grey.shade600, fontSize: 12), - ), - const SizedBox(height: 16), - _buildStatRow( - 'Ecart type X (horizontal)', - '${(stdDev.stdDevX * 100).toStringAsFixed(2)}%', - ), - _buildStatRow( - 'Ecart type Y (vertical)', - '${(stdDev.stdDevY * 100).toStringAsFixed(2)}%', - ), - _buildStatRow( - 'Ecart type radial', - '${(stdDev.stdDevRadial * 100).toStringAsFixed(2)}%', - ), - _buildStatRow( - 'Ecart type score', - stdDev.stdDevScore.toStringAsFixed(2), - ), - const Divider(height: 24), - _buildStatRow( - 'Position moyenne X', - '${(stdDev.meanX * 100).toStringAsFixed(1)}%', - ), - _buildStatRow( - 'Position moyenne Y', - '${(stdDev.meanY * 100).toStringAsFixed(1)}%', - ), - _buildStatRow('Score moyen', stdDev.meanScore.toStringAsFixed(2)), - ], - ), - ), - ); - } - - Widget _buildRegionalSection() { - final regional = _statistics!.regional; - - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Icon(Icons.explore, color: AppTheme.secondaryColor), - const SizedBox(width: 8), - const Text( - 'Distribution Regionale', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), - ], - ), - const SizedBox(height: 16), - - // Dominant direction - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: AppTheme.primaryColor.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Row( + : SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( children: [ - const Icon( - Icons.compass_calibration, - color: AppTheme.primaryColor, + // 1. FILTRES (Arme et Distance) + Row( + children: [ + Expanded( + child: _buildDropdown('Arme utilisée', _selectedWeapon), + ), + const SizedBox(width: 12), + Expanded( + child: _buildDropdown('Distance', _selectedDistance), + ), + ], ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Direction dominante'), - Text( - regional.dominantDirection, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), + const SizedBox(height: 20), + + // 2. DONNÉES RAPIDES (Tirs et Sessions) + Row( + children: [ + Expanded( + child: _buildQuickStat( + 'Nbre de tirs', + '${_statistics?.totalShots ?? 0}', + Icons.gps_fixed, ), - ], + ), + const SizedBox(width: 12), + Expanded( + child: _buildQuickStat( + 'Nbre de sessions', + '${_statistics?.sessions.length ?? 0}', + Icons.analytics, + ), + ), + ], + ), + const SizedBox(height: 25), + + // 3. LES GRAPHIQUES + _buildChartSection( + 'Score', + '${_statistics?.totalScore ?? 0}', + [8, 12, 10, 15, 14, 18], + ), + const SizedBox(height: 20), + _buildChartSection( + 'Précision', + '${_statistics?.precision.precisionScore.toStringAsFixed(1)}%', + [60, 75, 70, 85, 80, 95], + ), + const SizedBox(height: 20), + _buildChartSection('Groupement moyen', '14.2 mm', [ + 20, + 18, + 22, + 15, + 14, + 12, + ]), // mm fictif pour l'exemple + + const SizedBox(height: 30), + + // 4. BOUTON ACCUEIL + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF1A73E8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () => Navigator.of( + context, + ).popUntil((route) => route.isFirst), + child: const Text( + 'ACCUEIL', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), ), ), ], ), ), - const SizedBox(height: 16), - - // Bias - if (regional.biasX.abs() > 0.02 || regional.biasY.abs() > 0.02) - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: AppTheme.warningColor.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: [ - const Icon( - Icons.warning_amber, - color: AppTheme.warningColor, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Biais detecte'), - Text( - _getBiasDescription(regional.biasX, regional.biasY), - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ], - ), - ), - ], - ), - ), - const SizedBox(height: 16), - - // Sector distribution - const Text( - 'Repartition par secteur:', - style: TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - Wrap( - spacing: 8, - runSpacing: 8, - children: regional.sectorDistribution.entries.map((entry) { - final percentage = _statistics!.totalShots > 0 - ? (entry.value / _statistics!.totalShots * 100) - : 0.0; - return _buildSectorChip(entry.key, entry.value, percentage); - }).toList(), - ), - - const SizedBox(height: 16), - - // Quadrant distribution - const Text( - 'Repartition par quadrant:', - style: TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - _buildQuadrantGrid(regional.quadrantDistribution), - ], - ), - ), ); } - Widget _buildStatRow(String label, String value) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + // Widget pour les Dropdowns de filtres + Widget _buildDropdown(String label, String value) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: const Color(0xFF1E1E1E), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.white12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(label), - Text(value, style: const TextStyle(fontWeight: FontWeight.bold)), + Text( + label, + style: const TextStyle(color: Colors.white54, fontSize: 10), + ), + DropdownButton( + value: value, + isExpanded: true, + underline: Container(), + dropdownColor: const Color(0xFF1E1E1E), + style: const TextStyle(color: Colors.white, fontSize: 14), + items: [value] + .map( + (String val) => + DropdownMenuItem(value: val, child: Text(val)), + ) + .toList(), + onChanged: (newValue) {}, + ), ], ), ); } - Widget _buildSectorChip(String sector, int count, double percentage) { + // Widget pour les petites cartes de stats + Widget _buildQuickStat(String label, String value, IconData icon) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: count > 0 - ? AppTheme.primaryColor.withValues(alpha: 0.1) - : Colors.grey.shade100, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: count > 0 ? AppTheme.primaryColor : Colors.grey.shade300, - ), + color: const Color(0xFF1E1E1E), + borderRadius: BorderRadius.circular(12), ), - child: Text( - '$sector: $count (${percentage.toStringAsFixed(0)}%)', - style: TextStyle( - fontSize: 12, - color: count > 0 ? AppTheme.primaryColor : Colors.grey.shade600, - ), + child: Column( + children: [ + Icon(icon, color: const Color(0xFF1A73E8), size: 20), + const SizedBox(height: 8), + Text( + value, + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + Text( + label, + style: const TextStyle(color: Colors.white54, fontSize: 12), + ), + ], ), ); } - Widget _buildQuadrantGrid(Map quadrants) { - return Table( - border: TableBorder.all(color: Colors.grey.shade300), - children: [ - TableRow( - children: [ - _buildQuadrantCell('Haut-Gauche', quadrants['Haut-Gauche'] ?? 0), - _buildQuadrantCell('Haut-Droite', quadrants['Haut-Droite'] ?? 0), - ], - ), - TableRow( - children: [ - _buildQuadrantCell('Bas-Gauche', quadrants['Bas-Gauche'] ?? 0), - _buildQuadrantCell('Bas-Droite', quadrants['Bas-Droite'] ?? 0), - ], + // Widget pour une section de graphique + Widget _buildChartSection( + String title, + String value, + List dataPoints, + ) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF1E1E1E), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle(color: Colors.white70, fontSize: 14), + ), + const SizedBox(height: 10), + Row( + children: [ + Text( + value, + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 20), + Expanded( + child: SizedBox( + height: 60, + child: LineChart(_mainChartData(dataPoints)), + ), + ), + ], + ), + ], + ), + ); + } + + LineChartData _mainChartData(List points) { + return LineChartData( + gridData: const FlGridData(show: false), + titlesData: const FlTitlesData(show: false), + borderData: FlBorderData(show: false), + lineBarsData: [ + LineChartBarData( + spots: points + .asMap() + .entries + .map((e) => FlSpot(e.key.toDouble(), e.value)) + .toList(), + isCurved: true, + color: const Color(0xFF4CAF50), // Vert comme sur ton design + barWidth: 3, + isStrokeCapRound: true, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData( + show: true, + color: const Color(0xFF4CAF50).withOpacity(0.2), + ), ), ], ); } - - Widget _buildQuadrantCell(String label, int count) { - final percentage = _statistics!.totalShots > 0 - ? (count / _statistics!.totalShots * 100) - : 0.0; - final intensity = _statistics!.totalShots > 0 - ? count / _statistics!.totalShots - : 0.0; - - return Container( - padding: const EdgeInsets.all(16), - color: Color.lerp(Colors.white, AppTheme.primaryColor, intensity * 0.5), - child: Column( - children: [ - Text( - '$count', - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 24), - ), - Text( - '${percentage.toStringAsFixed(0)}%', - style: TextStyle(color: Colors.grey.shade600), - ), - Text( - label, - style: const TextStyle(fontSize: 10), - textAlign: TextAlign.center, - ), - ], - ), - ); - } - - String _getBiasDescription(double biasX, double biasY) { - final descriptions = []; - - if (biasX.abs() > 0.02) { - descriptions.add(biasX > 0 ? 'vers la droite' : 'vers la gauche'); - } - if (biasY.abs() > 0.02) { - descriptions.add(biasY > 0 ? 'vers le bas' : 'vers le haut'); - } - - return 'Tendance ${descriptions.join(' et ')}'; - } -} - -class _StatCard extends StatelessWidget { - final IconData icon; - final String title; - final String value; - final Color color; - - const _StatCard({ - required this.icon, - required this.title, - required this.value, - required this.color, - }); - - @override - Widget build(BuildContext context) { - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Icon(icon, color: color, size: 32), - const SizedBox(height: 8), - Text( - value, - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: color, - ), - ), - Text(title, style: TextStyle(color: Colors.grey.shade600)), - ], - ), - ), - ); - } } diff --git a/pubspec.lock b/pubspec.lock index c7627e42..8bdb21ac 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -197,10 +197,10 @@ packages: dependency: "direct main" description: name: fl_chart - sha256: "00b74ae680df6b1135bdbea00a7d1fc072a9180b7c3f3702e4b19a9943f5ed7d" + sha256: b938f77d042cbcd822936a7a359a7235bad8bd72070de1f827efc2cc297ac888 url: "https://pub.dev" source: hosted - version: "0.66.2" + version: "1.2.0" flutter: dependency: "direct main" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index fe872042..e60c99ef 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,7 +61,7 @@ dependencies: intl: ^0.20.2 # Charts for history visualization - fl_chart: ^0.66.2 + fl_chart: ^1.2.0 # Image processing for impact detection image: ^4.1.7