Version propre - Dashboard enhanced
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../shared/theme/design_system.dart';
|
||||
|
||||
/// Graphique en secteurs professionnel avec animations et légendes
|
||||
class ProfessionalPieChart extends StatefulWidget {
|
||||
const ProfessionalPieChart({
|
||||
super.key,
|
||||
required this.data,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.centerText,
|
||||
this.showLegend = true,
|
||||
this.showPercentages = true,
|
||||
this.animationDuration = const Duration(milliseconds: 1500),
|
||||
});
|
||||
|
||||
final List<ChartDataPoint> data;
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final String? centerText;
|
||||
final bool showLegend;
|
||||
final bool showPercentages;
|
||||
final Duration animationDuration;
|
||||
|
||||
@override
|
||||
State<ProfessionalPieChart> createState() => _ProfessionalPieChartState();
|
||||
}
|
||||
|
||||
class _ProfessionalPieChartState extends State<ProfessionalPieChart>
|
||||
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: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: _buildChart(),
|
||||
),
|
||||
if (widget.showLegend) ...[
|
||||
SizedBox(width: DesignSystem.spacingLg),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _buildLegend(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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 Container(
|
||||
height: 140, // Hauteur encore plus réduite
|
||||
padding: const EdgeInsets.all(4), // Padding minimal pour contenir le graphique
|
||||
child: PieChart(
|
||||
PieChartData(
|
||||
pieTouchData: PieTouchData(
|
||||
touchCallback: (FlTouchEvent event, pieTouchResponse) {
|
||||
setState(() {
|
||||
if (!event.isInterestedForInteractions ||
|
||||
pieTouchResponse == null ||
|
||||
pieTouchResponse.touchedSection == null) {
|
||||
_touchedIndex = -1;
|
||||
return;
|
||||
}
|
||||
_touchedIndex = pieTouchResponse.touchedSection!.touchedSectionIndex;
|
||||
});
|
||||
},
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
sectionsSpace: 1, // Espace réduit entre sections
|
||||
centerSpaceRadius: widget.centerText != null ? 45 : 30, // Rayon central réduit
|
||||
sections: _buildSections(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<PieChartSectionData> _buildSections() {
|
||||
final total = widget.data.fold<double>(0, (sum, item) => sum + item.value);
|
||||
|
||||
return widget.data.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final data = entry.value;
|
||||
final isTouched = index == _touchedIndex;
|
||||
final percentage = (data.value / total * 100);
|
||||
|
||||
return PieChartSectionData(
|
||||
color: data.color,
|
||||
value: data.value * _animation.value,
|
||||
title: widget.showPercentages ? '${percentage.toStringAsFixed(1)}%' : '',
|
||||
radius: isTouched ? 70 : 60,
|
||||
titleStyle: DesignSystem.labelMedium.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
offset: const Offset(1, 1),
|
||||
blurRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
titlePositionPercentageOffset: 0.6,
|
||||
badgeWidget: isTouched ? _buildBadge(data) : null,
|
||||
badgePositionPercentageOffset: 1.3,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Widget _buildBadge(ChartDataPoint data) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: DesignSystem.spacingSm,
|
||||
vertical: DesignSystem.spacingXs,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: data.color,
|
||||
borderRadius: BorderRadius.circular(DesignSystem.radiusSm),
|
||||
boxShadow: DesignSystem.shadowCard,
|
||||
),
|
||||
child: Text(
|
||||
data.value.toInt().toString(),
|
||||
style: DesignSystem.labelMedium.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLegend() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (widget.centerText != null) ...[
|
||||
_buildCenterInfo(),
|
||||
SizedBox(height: DesignSystem.spacingLg),
|
||||
],
|
||||
...widget.data.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final data = entry.value;
|
||||
final isSelected = index == _touchedIndex;
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: DesignSystem.animationFast,
|
||||
margin: EdgeInsets.only(bottom: DesignSystem.spacingSm),
|
||||
padding: EdgeInsets.all(DesignSystem.spacingSm),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? data.color.withOpacity(0.1) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(DesignSystem.radiusSm),
|
||||
border: isSelected ? Border.all(
|
||||
color: data.color.withOpacity(0.3),
|
||||
width: 1,
|
||||
) : null,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: data.color,
|
||||
borderRadius: BorderRadius.circular(DesignSystem.radiusXs),
|
||||
),
|
||||
),
|
||||
SizedBox(width: DesignSystem.spacingSm),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
data.label,
|
||||
style: DesignSystem.labelLarge.copyWith(
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
data.value.toInt().toString(),
|
||||
style: DesignSystem.labelMedium.copyWith(
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCenterInfo() {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(DesignSystem.spacingMd),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DesignSystem.radiusMd),
|
||||
border: Border.all(
|
||||
color: AppTheme.primaryColor.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Total',
|
||||
style: DesignSystem.labelMedium.copyWith(
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
SizedBox(height: DesignSystem.spacingXs),
|
||||
Text(
|
||||
widget.centerText!,
|
||||
style: DesignSystem.headlineMedium.copyWith(
|
||||
color: AppTheme.primaryColor,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Modèle de données pour le graphique en secteurs
|
||||
class ChartDataPoint {
|
||||
const ChartDataPoint({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final double value;
|
||||
final Color color;
|
||||
}
|
||||
Reference in New Issue
Block a user