251 lines
8.4 KiB
Dart
251 lines
8.4 KiB
Dart
/// 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<Session> 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<Session>.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<Session> 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<Session> displaySessions) {
|
|
final avgScore = displaySessions.fold<int>(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,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|