Files
unionflow-client-quarkus-pr…/unionflow-mobile-apps/lib/features/members/presentation/widgets/professional_pie_chart.dart
2025-09-13 19:05:06 +00:00

308 lines
9.0 KiB
Dart

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;
}