import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; /// A single labelled bar in [StatBarChart]. class StatBarDatum { const StatBarDatum({required this.label, required this.value}); /// Short label rendered along the X axis (kept terse so it fits). final String label; final int value; } /// Lightweight wrapper around [BarChart] for the top-6 leaderboards. Renders /// vertical bars with a numeric Y axis and the supplied [data] labels on X. class StatBarChart extends StatelessWidget { const StatBarChart({ super.key, required this.data, required this.valueLabel, this.height = 280, }); /// Sorted list of bars to render (highest first); only the first 6 are used. final List data; /// Used for the Y axis title, e.g. "Goals". final String valueLabel; final double height; @override Widget build(BuildContext context) { final theme = Theme.of(context); final scheme = theme.colorScheme; final visible = data.take(6).toList(growable: false); if (visible.isEmpty) { return SizedBox( height: height, child: Center( child: Text( 'No data yet', style: theme.textTheme.bodyMedium?.copyWith( color: scheme.onSurfaceVariant, ), ), ), ); } final maxValue = visible .map((d) => d.value) .fold(0, (a, b) => a > b ? a : b); // Round the y-axis ceiling up to the nearest sensible interval so the // grid lines land on whole numbers. final yMax = maxValue <= 4 ? 4.0 : (maxValue + 2).toDouble(); final interval = yMax <= 6 ? 1.0 : (yMax / 5).ceilToDouble(); return Padding( padding: const EdgeInsets.fromLTRB(12, 4, 16, 8), child: SizedBox( height: height, child: BarChart( BarChartData( alignment: BarChartAlignment.spaceAround, maxY: yMax, minY: 0, barTouchData: BarTouchData( enabled: true, touchTooltipData: BarTouchTooltipData( getTooltipColor: (_) => scheme.surfaceContainerHigh, tooltipBorder: BorderSide(color: scheme.outlineVariant), getTooltipItem: (group, _, rod, _) { final datum = visible[group.x]; return BarTooltipItem( '${datum.label}\n', theme.textTheme.bodySmall!.copyWith( color: scheme.onSurface, fontWeight: FontWeight.w700, ), children: [ TextSpan( text: '${rod.toY.toInt()} $valueLabel', style: theme.textTheme.bodySmall?.copyWith( color: scheme.primary, fontWeight: FontWeight.w700, ), ), ], ); }, ), ), gridData: FlGridData( show: true, drawVerticalLine: false, horizontalInterval: interval, getDrawingHorizontalLine: (_) => FlLine( color: scheme.outlineVariant.withValues(alpha: 0.4), strokeWidth: 1, ), ), borderData: FlBorderData(show: false), titlesData: FlTitlesData( show: true, topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), leftTitles: AxisTitles( axisNameWidget: Padding( padding: const EdgeInsets.only(bottom: 6), child: Text( valueLabel, style: theme.textTheme.labelSmall?.copyWith( color: scheme.onSurfaceVariant, letterSpacing: 0.5, ), ), ), axisNameSize: 18, sideTitles: SideTitles( showTitles: true, interval: interval, reservedSize: 32, getTitlesWidget: (value, meta) { if (value == 0 || value > yMax) { return const SizedBox.shrink(); } return Padding( padding: const EdgeInsets.only(right: 4), child: Text( value.toInt().toString(), style: theme.textTheme.labelSmall?.copyWith( color: scheme.onSurfaceVariant, ), ), ); }, ), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 36, interval: 1, getTitlesWidget: (value, meta) { final index = value.toInt(); if (index < 0 || index >= visible.length) { return const SizedBox.shrink(); } return Padding( padding: const EdgeInsets.only(top: 6), child: Text( _shortLabel(visible[index].label), textAlign: TextAlign.center, style: theme.textTheme.labelSmall?.copyWith( color: scheme.onSurfaceVariant, fontWeight: FontWeight.w600, ), ), ); }, ), ), ), barGroups: [ for (var i = 0; i < visible.length; i++) BarChartGroupData( x: i, barRods: [ BarChartRodData( toY: visible[i].value.toDouble(), color: scheme.primary, width: 22, borderRadius: const BorderRadius.vertical( top: Radius.circular(6), ), backDrawRodData: BackgroundBarChartRodData( show: true, toY: yMax, color: scheme.surfaceContainerHighest .withValues(alpha: 0.5), ), ), ], ), ], ), ), ), ); } /// Compresses long names like "Marcus Reed" → "M. Reed" so X-axis labels /// don't overflow on phone widths. static String _shortLabel(String full) { final parts = full.trim().split(RegExp(r'\s+')); if (parts.length < 2) { return parts.first.length > 10 ? '${parts.first.substring(0, 9)}…' : parts.first; } final last = parts.last; final first = parts.first; final initial = first.isEmpty ? '' : '${first[0]}. '; final candidate = '$initial$last'; if (candidate.length <= 12) return candidate; return '${candidate.substring(0, 11)}…'; } }