From 1d8124c8d8dca8630027a0ab618019cad25b2a24 Mon Sep 17 00:00:00 2001 From: qguillaume Date: Tue, 16 Jun 2026 10:26:54 +0200 Subject: [PATCH] fix: clarifie les libelles de stats et corrige le calcul de l etalement moyen - score_card: "Pourcentage" -> "Reussite", ajoute "/10" sur la moyenne - grouping_stats: "Diametre" -> "Etalement", direction du decalage en toutes lettres (Droite, Haut-Droite...) au lieu de D/G/H/B - statistics: corrige le bug de priorite (... ?? 0 * 100) qui affichait un etalement moyen errone (~0.9% au lieu de ~90%) - ajoute un bouton d aide reutilisable (MetricInfoButton) expliquant chaque metrique, dont la distinction Precision (distance) / Reussite (score) Co-Authored-By: Claude Opus 4.8 --- lib/core/widgets/metric_info_button.dart | 76 ++++++++++++++++++ .../analysis/widgets/grouping_stats.dart | 78 ++++++++++++++----- lib/features/analysis/widgets/score_card.dart | 29 ++++++- .../statistics/statistics_screen.dart | 52 +++++++++++-- 4 files changed, 206 insertions(+), 29 deletions(-) create mode 100644 lib/core/widgets/metric_info_button.dart diff --git a/lib/core/widgets/metric_info_button.dart b/lib/core/widgets/metric_info_button.dart new file mode 100644 index 00000000..d6f605a4 --- /dev/null +++ b/lib/core/widgets/metric_info_button.dart @@ -0,0 +1,76 @@ +/// Bouton d'information (ⓘ) qui explique des métriques à l'utilisateur. +/// +/// Affiche une petite icône cliquable ; au clic, une boîte de dialogue +/// détaille la signification de chaque statistique de la carte associée. +library; + +import 'package:flutter/material.dart'; + +/// Explication d'une métrique : un libellé et sa description. +class MetricExplanation { + final String label; + final String description; + + const MetricExplanation(this.label, this.description); +} + +class MetricInfoButton extends StatelessWidget { + final String title; + final List explanations; + + const MetricInfoButton({ + super.key, + required this.title, + required this.explanations, + }); + + @override + Widget build(BuildContext context) { + return IconButton( + icon: Icon(Icons.info_outline, size: 18, color: Colors.grey[500]), + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + tooltip: 'À quoi ça correspond ?', + onPressed: () => _showInfo(context), + ); + } + + void _showInfo(BuildContext context) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(title), + content: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + for (final e in explanations) + Padding( + padding: const EdgeInsets.only(bottom: 14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + e.label, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 2), + Text(e.description), + ], + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Compris'), + ), + ], + ), + ); + } +} diff --git a/lib/features/analysis/widgets/grouping_stats.dart b/lib/features/analysis/widgets/grouping_stats.dart index 96889e25..152cbbb0 100644 --- a/lib/features/analysis/widgets/grouping_stats.dart +++ b/lib/features/analysis/widgets/grouping_stats.dart @@ -7,6 +7,7 @@ library; import 'package:flutter/material.dart'; import '../../../core/constants/app_constants.dart'; import '../../../core/theme/app_theme.dart'; +import '../../../core/widgets/metric_info_button.dart'; import '../../../services/grouping_analyzer_service.dart'; class GroupingStats extends StatelessWidget { @@ -43,31 +44,64 @@ class GroupingStats extends StatelessWidget { fontWeight: FontWeight.bold, ), ), + const MetricInfoButton( + title: 'Groupement', + explanations: [ + MetricExplanation( + 'Étalement', + 'Distance entre vos deux impacts les plus éloignés, ' + 'exprimée en % de la largeur de l\'image. Plus c\'est ' + 'bas, plus le groupement est serré.', + ), + MetricExplanation( + 'Dispersion', + 'Régularité des impacts autour de leur centre commun ' + '(écart-type). Plus c\'est bas, plus vos tirs sont ' + 'réguliers.', + ), + MetricExplanation( + 'Décalage', + 'Direction du centre de votre groupement par rapport au ' + 'centre de la cible (ex. « Droite » = vos tirs sont ' + 'globalement décalés vers la droite).', + ), + MetricExplanation( + 'Étoiles', + 'Qualité globale du groupement, de ★ (à améliorer) à ' + '★★★★★ (excellent).', + ), + ], + ), const Spacer(), _buildQualityBadge(context), ], ), const Divider(), Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - _buildStat( - context, - 'Diametre', - '${(groupingResult.diameter * 100).toStringAsFixed(1)}%', - icon: Icons.straighten, + Expanded( + child: _buildStat( + context, + 'Étalement', + '${(groupingResult.diameter * 100).toStringAsFixed(1)}%', + icon: Icons.straighten, + ), ), - _buildStat( - context, - 'Dispersion', - '${(groupingResult.standardDeviation * 100).toStringAsFixed(1)}%', - icon: Icons.scatter_plot, + Expanded( + child: _buildStat( + context, + 'Dispersion', + '${(groupingResult.standardDeviation * 100).toStringAsFixed(1)}%', + icon: Icons.scatter_plot, + ), ), - _buildStat( - context, - 'Decalage', - offsetDescription, - icon: Icons.compare_arrows, + Expanded( + child: _buildStat( + context, + 'Décalage', + offsetDescription, + icon: Icons.compare_arrows, + ), ), ], ), @@ -125,12 +159,14 @@ class GroupingStats extends StatelessWidget { const SizedBox(height: 4), Text( value, + textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), Text( label, + textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall, ), ], @@ -252,22 +288,22 @@ class GroupingStats extends StatelessWidget { String _getOffsetDescription(double offsetX, double offsetY) { if (offsetX.abs() < 0.02 && offsetY.abs() < 0.02) { - return 'Centre'; + return 'Centré'; } String vertical = ''; String horizontal = ''; if (offsetY < -0.02) { - vertical = 'H'; + vertical = 'Haut'; } else if (offsetY > 0.02) { - vertical = 'B'; + vertical = 'Bas'; } if (offsetX < -0.02) { - horizontal = 'G'; + horizontal = 'Gauche'; } else if (offsetX > 0.02) { - horizontal = 'D'; + horizontal = 'Droite'; } if (vertical.isNotEmpty && horizontal.isNotEmpty) { diff --git a/lib/features/analysis/widgets/score_card.dart b/lib/features/analysis/widgets/score_card.dart index 267557cc..2b45d70d 100644 --- a/lib/features/analysis/widgets/score_card.dart +++ b/lib/features/analysis/widgets/score_card.dart @@ -7,6 +7,7 @@ library; import 'package:flutter/material.dart'; import '../../../core/constants/app_constants.dart'; import '../../../core/theme/app_theme.dart'; +import '../../../core/widgets/metric_info_button.dart'; import '../../../data/models/target_type.dart'; import '../../../services/score_calculator_service.dart'; @@ -44,6 +45,31 @@ class ScoreCard extends StatelessWidget { fontWeight: FontWeight.bold, ), ), + const Spacer(), + MetricInfoButton( + title: 'Score', + explanations: [ + MetricExplanation( + 'Total', + 'Somme des points de tous vos impacts, sur le maximum ' + 'possible (nombre d\'impacts × $maxScore points).', + ), + const MetricExplanation( + 'Impacts', + 'Nombre de tirs détectés sur la cible.', + ), + MetricExplanation( + 'Moyenne', + 'Points marqués en moyenne par impact, sur $maxScore.', + ), + const MetricExplanation( + 'Réussite', + 'Votre score exprimé en pourcentage du score maximum ' + 'possible. C\'est une mesure du résultat, pas de la ' + 'régularité des tirs.', + ), + ], + ), ], ), const Divider(), @@ -67,11 +93,12 @@ class ScoreCard extends StatelessWidget { shotCount > 0 ? (totalScore / shotCount).toStringAsFixed(1) : '-', + subtitle: '/ $maxScore', ), if (scoreResult != null) _buildScoreStat( context, - 'Pourcentage', + 'Réussite', '${scoreResult!.percentage.toStringAsFixed(0)}%', ), ], diff --git a/lib/features/statistics/statistics_screen.dart b/lib/features/statistics/statistics_screen.dart index 43b32640..a5e2bc6a 100644 --- a/lib/features/statistics/statistics_screen.dart +++ b/lib/features/statistics/statistics_screen.dart @@ -3,6 +3,7 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:fl_chart/fl_chart.dart'; // La librairie pour les courbes +import '../../core/widgets/metric_info_button.dart'; import '../../data/models/session.dart'; import '../../data/repositories/session_repository.dart'; import '../../services/statistics_service.dart'; @@ -191,18 +192,46 @@ class _StatisticsScreenState extends State { 'Score', '${_statistics?.totalScore ?? 0}', _getScoreHistory(), + explanations: const [ + MetricExplanation( + 'Score', + 'Total des points marqués sur la période sélectionnée.', + ), + ], ), const SizedBox(height: 20), _buildChartSection( 'Précision', '${_statistics?.precision.precisionScore.toStringAsFixed(1)}%', _getPrecisionHistory(), + explanations: const [ + MetricExplanation( + 'Précision', + 'Proximité moyenne de vos impacts par rapport au centre ' + 'de la cible (calculée sur les distances).', + ), + MetricExplanation( + 'À ne pas confondre', + 'C\'est différent de la « Réussite » affichée dans une ' + 'session, qui se base sur les points marqués. Les deux ' + 'mesurent des choses distinctes, leurs pourcentages ' + 'ne sont donc pas identiques.', + ), + ], ), const SizedBox(height: 20), _buildChartSection( - 'Groupement moyen', - '${(_statistics?.precision.groupingDiameter ?? 0 * 100).toStringAsFixed(1)}%', + 'Étalement moyen', + '${((_statistics?.precision.groupingDiameter ?? 0) * 100).toStringAsFixed(1)}%', _getScoreHistory().map((e) => e / 10).toList(), // Proxy for grouping history + explanations: const [ + MetricExplanation( + 'Étalement moyen', + 'Étalement moyen de vos groupements : distance entre les ' + 'impacts les plus éloignés, en % de la largeur de ' + 'l\'image. Plus c\'est bas, plus vos tirs sont serrés.', + ), + ], ), const SizedBox(height: 25), @@ -302,8 +331,9 @@ class _StatisticsScreenState extends State { Widget _buildChartSection( String title, String value, - List dataPoints, - ) { + List dataPoints, { + List? explanations, + }) { final theme = Theme.of(context); return Container( padding: const EdgeInsets.all(16), @@ -314,9 +344,17 @@ class _StatisticsScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - title, - style: TextStyle(color: theme.textTheme.bodyMedium?.color?.withValues(alpha: 0.7), fontSize: 14), + Row( + children: [ + Text( + title, + style: TextStyle(color: theme.textTheme.bodyMedium?.color?.withValues(alpha: 0.7), fontSize: 14), + ), + if (explanations != null) ...[ + const Spacer(), + MetricInfoButton(title: title, explanations: explanations), + ], + ], ), const SizedBox(height: 10), Row(