270 lines
7.7 KiB
Dart
270 lines
7.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:fl_chart/fl_chart.dart';
|
|
import 'package:intl/intl.dart';
|
|
import '../../../../shared/theme/app_theme.dart';
|
|
import '../../../../shared/theme/design_system.dart';
|
|
|
|
/// Graphique en barres professionnel avec animations et interactions
|
|
class ProfessionalBarChart extends StatefulWidget {
|
|
const ProfessionalBarChart({
|
|
super.key,
|
|
required this.data,
|
|
required this.title,
|
|
this.subtitle,
|
|
this.showGrid = true,
|
|
this.showValues = true,
|
|
this.animationDuration = const Duration(milliseconds: 1500),
|
|
this.barColor,
|
|
this.gradientColors,
|
|
});
|
|
|
|
final List<BarDataPoint> data;
|
|
final String title;
|
|
final String? subtitle;
|
|
final bool showGrid;
|
|
final bool showValues;
|
|
final Duration animationDuration;
|
|
final Color? barColor;
|
|
final List<Color>? gradientColors;
|
|
|
|
@override
|
|
State<ProfessionalBarChart> createState() => _ProfessionalBarChartState();
|
|
}
|
|
|
|
class _ProfessionalBarChartState extends State<ProfessionalBarChart>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _animationController;
|
|
late Animation<double> _animation;
|
|
int _touchedIndex = -1;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_animationController = AnimationController(
|
|
duration: widget.animationDuration,
|
|
vsync: this,
|
|
);
|
|
|
|
_animation = Tween<double>(
|
|
begin: 0.0,
|
|
end: 1.0,
|
|
).animate(CurvedAnimation(
|
|
parent: _animationController,
|
|
curve: DesignSystem.animationCurve,
|
|
));
|
|
|
|
_animationController.forward();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_animationController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildHeader(),
|
|
SizedBox(height: DesignSystem.spacingLg),
|
|
Expanded(
|
|
child: _buildChart(),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildHeader() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
widget.title,
|
|
style: DesignSystem.titleLarge.copyWith(
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
if (widget.subtitle != null) ...[
|
|
SizedBox(height: DesignSystem.spacingXs),
|
|
Text(
|
|
widget.subtitle!,
|
|
style: DesignSystem.bodyMedium.copyWith(
|
|
color: AppTheme.textSecondary,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildChart() {
|
|
return AnimatedBuilder(
|
|
animation: _animation,
|
|
builder: (context, child) {
|
|
return BarChart(
|
|
BarChartData(
|
|
alignment: BarChartAlignment.spaceAround,
|
|
maxY: _getMaxY() * 1.2,
|
|
barTouchData: BarTouchData(
|
|
touchTooltipData: BarTouchTooltipData(
|
|
tooltipBgColor: AppTheme.textPrimary.withOpacity(0.9),
|
|
tooltipRoundedRadius: DesignSystem.radiusSm,
|
|
tooltipPadding: EdgeInsets.all(DesignSystem.spacingSm),
|
|
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
|
return BarTooltipItem(
|
|
'${widget.data[groupIndex].label}\n${rod.toY.toInt()}',
|
|
DesignSystem.labelMedium.copyWith(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
touchCallback: (FlTouchEvent event, barTouchResponse) {
|
|
setState(() {
|
|
if (!event.isInterestedForInteractions ||
|
|
barTouchResponse == null ||
|
|
barTouchResponse.spot == null) {
|
|
_touchedIndex = -1;
|
|
return;
|
|
}
|
|
_touchedIndex = barTouchResponse.spot!.touchedBarGroupIndex;
|
|
});
|
|
},
|
|
),
|
|
titlesData: FlTitlesData(
|
|
show: true,
|
|
rightTitles: const AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false),
|
|
),
|
|
topTitles: const AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false),
|
|
),
|
|
bottomTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
getTitlesWidget: _buildBottomTitles,
|
|
reservedSize: 42,
|
|
),
|
|
),
|
|
leftTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
getTitlesWidget: _buildLeftTitles,
|
|
reservedSize: 40,
|
|
),
|
|
),
|
|
),
|
|
borderData: FlBorderData(show: false),
|
|
gridData: FlGridData(
|
|
show: widget.showGrid,
|
|
drawVerticalLine: false,
|
|
horizontalInterval: _getMaxY() / 5,
|
|
getDrawingHorizontalLine: (value) {
|
|
return FlLine(
|
|
color: AppTheme.borderColor.withOpacity(0.3),
|
|
strokeWidth: 1,
|
|
);
|
|
},
|
|
),
|
|
barGroups: _buildBarGroups(),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
List<BarChartGroupData> _buildBarGroups() {
|
|
return widget.data.asMap().entries.map((entry) {
|
|
final index = entry.key;
|
|
final data = entry.value;
|
|
final isTouched = index == _touchedIndex;
|
|
|
|
return BarChartGroupData(
|
|
x: index,
|
|
barRods: [
|
|
BarChartRodData(
|
|
toY: data.value * _animation.value,
|
|
color: _getBarColor(index, isTouched),
|
|
width: isTouched ? 24 : 20,
|
|
borderRadius: BorderRadius.only(
|
|
topLeft: Radius.circular(DesignSystem.radiusXs),
|
|
topRight: Radius.circular(DesignSystem.radiusXs),
|
|
),
|
|
gradient: widget.gradientColors != null ? LinearGradient(
|
|
begin: Alignment.bottomCenter,
|
|
end: Alignment.topCenter,
|
|
colors: widget.gradientColors!,
|
|
) : null,
|
|
),
|
|
],
|
|
showingTooltipIndicators: isTouched ? [0] : [],
|
|
);
|
|
}).toList();
|
|
}
|
|
|
|
Color _getBarColor(int index, bool isTouched) {
|
|
if (widget.barColor != null) {
|
|
return isTouched
|
|
? widget.barColor!
|
|
: widget.barColor!.withOpacity(0.8);
|
|
}
|
|
|
|
final colors = DesignSystem.chartColors;
|
|
final color = colors[index % colors.length];
|
|
return isTouched ? color : color.withOpacity(0.8);
|
|
}
|
|
|
|
Widget _buildBottomTitles(double value, TitleMeta meta) {
|
|
if (value.toInt() >= widget.data.length) return const SizedBox.shrink();
|
|
|
|
final data = widget.data[value.toInt()];
|
|
return SideTitleWidget(
|
|
axisSide: meta.axisSide,
|
|
child: Padding(
|
|
padding: EdgeInsets.only(top: DesignSystem.spacingXs),
|
|
child: Text(
|
|
data.label,
|
|
style: DesignSystem.labelSmall.copyWith(
|
|
color: AppTheme.textSecondary,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLeftTitles(double value, TitleMeta meta) {
|
|
return SideTitleWidget(
|
|
axisSide: meta.axisSide,
|
|
child: Text(
|
|
value.toInt().toString(),
|
|
style: DesignSystem.labelSmall.copyWith(
|
|
color: AppTheme.textSecondary,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
double _getMaxY() {
|
|
if (widget.data.isEmpty) return 10;
|
|
return widget.data.map((e) => e.value).reduce((a, b) => a > b ? a : b);
|
|
}
|
|
}
|
|
|
|
/// Modèle de données pour le graphique en barres
|
|
class BarDataPoint {
|
|
const BarDataPoint({
|
|
required this.label,
|
|
required this.value,
|
|
this.color,
|
|
});
|
|
|
|
final String label;
|
|
final double value;
|
|
final Color? color;
|
|
}
|