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 <noreply@anthropic.com>
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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)}%',
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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<StatisticsScreen> {
|
||||
'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<StatisticsScreen> {
|
||||
Widget _buildChartSection(
|
||||
String title,
|
||||
String value,
|
||||
List<double> dataPoints,
|
||||
) {
|
||||
List<double> dataPoints, {
|
||||
List<MetricExplanation>? explanations,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -314,9 +344,17 @@ class _StatisticsScreenState extends State<StatisticsScreen> {
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user