/// Statistiques de groupement des impacts. /// /// Affiche le diamètre de groupement, l'écart-type et le décalage par rapport /// au centre de la cible. Inclut un indicateur de qualité (Excellent, Bon, Moyen, Faible). library; import 'package:flutter/material.dart'; import '../../../core/constants/app_constants.dart'; import '../../../core/theme/app_theme.dart'; import '../../../services/grouping_analyzer_service.dart'; class GroupingStats extends StatelessWidget { final GroupingResult groupingResult; final double targetCenterX; final double targetCenterY; const GroupingStats({ super.key, required this.groupingResult, required this.targetCenterX, required this.targetCenterY, }); @override Widget build(BuildContext context) { final offsetX = groupingResult.centerX - targetCenterX; final offsetY = groupingResult.centerY - targetCenterY; final offsetDescription = _getOffsetDescription(offsetX, offsetY); return Card( child: Padding( padding: const EdgeInsets.all(AppConstants.defaultPadding), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.center_focus_strong, color: AppTheme.groupingCenterColor), const SizedBox(width: 8), Text( 'Groupement', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), const Spacer(), _buildQualityBadge(context), ], ), const Divider(), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildStat( context, 'Diametre', '${(groupingResult.diameter * 100).toStringAsFixed(1)}%', icon: Icons.straighten, ), _buildStat( context, 'Dispersion', '${(groupingResult.standardDeviation * 100).toStringAsFixed(1)}%', icon: Icons.scatter_plot, ), _buildStat( context, 'Decalage', offsetDescription, icon: Icons.compare_arrows, ), ], ), const SizedBox(height: 12), _buildOffsetIndicator(context, offsetX, offsetY), ], ), ), ); } Widget _buildQualityBadge(BuildContext context) { final rating = groupingResult.qualityRating; final color = _getQualityColor(rating); return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( color: color.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ ...List.generate(5, (index) { return Icon( index < rating ? Icons.star : Icons.star_border, size: 16, color: color, ); }), const SizedBox(width: 4), Text( groupingResult.qualityDescription, style: TextStyle( color: color, fontWeight: FontWeight.bold, fontSize: 12, ), ), ], ), ); } Widget _buildStat( BuildContext context, String label, String value, { required IconData icon, }) { return Column( children: [ Icon(icon, size: 20, color: Colors.grey), const SizedBox(height: 4), Text( value, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), Text( label, style: Theme.of(context).textTheme.bodySmall, ), ], ); } Widget _buildOffsetIndicator(BuildContext context, double offsetX, double offsetY) { return Container( height: 80, decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(8), ), child: Stack( children: [ // Grid lines Center( child: Container( width: 1, color: Colors.grey[300], ), ), Center( child: Container( height: 1, color: Colors.grey[300], ), ), // Center point (target) const Center( child: Icon(Icons.add, size: 16, color: Colors.grey), ), // Grouping center LayoutBuilder( builder: (context, constraints) { // Scale offset for visualization (max 40 pixels from center) final maxOffset = 40.0; final scaledX = (offsetX * 200).clamp(-maxOffset, maxOffset); final scaledY = (offsetY * 200).clamp(-maxOffset, maxOffset); return Center( child: Transform.translate( offset: Offset(scaledX, scaledY), child: Container( width: 12, height: 12, decoration: BoxDecoration( color: AppTheme.groupingCenterColor, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), ), ), ), ); }, ), // Labels Positioned( top: 4, left: 0, right: 0, child: Text( 'Haut', textAlign: TextAlign.center, style: TextStyle(fontSize: 10, color: Colors.grey[600]), ), ), Positioned( bottom: 4, left: 0, right: 0, child: Text( 'Bas', textAlign: TextAlign.center, style: TextStyle(fontSize: 10, color: Colors.grey[600]), ), ), Positioned( left: 4, top: 0, bottom: 0, child: Center( child: Text( 'G', style: TextStyle(fontSize: 10, color: Colors.grey[600]), ), ), ), Positioned( right: 4, top: 0, bottom: 0, child: Center( child: Text( 'D', style: TextStyle(fontSize: 10, color: Colors.grey[600]), ), ), ), ], ), ); } Color _getQualityColor(int rating) { switch (rating) { case 5: return AppTheme.successColor; case 4: return Colors.lightGreen; case 3: return AppTheme.warningColor; case 2: return Colors.orange; default: return AppTheme.errorColor; } } String _getOffsetDescription(double offsetX, double offsetY) { if (offsetX.abs() < 0.02 && offsetY.abs() < 0.02) { return 'Centre'; } String vertical = ''; String horizontal = ''; if (offsetY < -0.02) { vertical = 'H'; } else if (offsetY > 0.02) { vertical = 'B'; } if (offsetX < -0.02) { horizontal = 'G'; } else if (offsetX > 0.02) { horizontal = 'D'; } if (vertical.isNotEmpty && horizontal.isNotEmpty) { return '$vertical-$horizontal'; } return vertical.isNotEmpty ? vertical : horizontal; } }