Version propre - Dashboard enhanced
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user