Initialisation du projet geosector complet (web + flutter)
This commit is contained in:
404
flutt/lib/presentation/widgets/charts/payment_pie_chart.dart
Normal file
404
flutt/lib/presentation/widgets/charts/payment_pie_chart.dart
Normal file
@@ -0,0 +1,404 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||
import 'package:geosector_app/presentation/widgets/charts/payment_data.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
/// Widget de graphique en camembert pour représenter la répartition des règlements
|
||||
class PaymentPieChart extends StatefulWidget {
|
||||
/// Liste des données de règlement à afficher dans le graphique
|
||||
final List<PaymentData> payments;
|
||||
|
||||
/// Taille du graphique
|
||||
final double size;
|
||||
|
||||
/// Taille des étiquettes
|
||||
final double labelSize;
|
||||
|
||||
/// Afficher les pourcentages
|
||||
final bool showPercentage;
|
||||
|
||||
/// Afficher les icônes
|
||||
final bool showIcons;
|
||||
|
||||
/// Afficher la légende
|
||||
final bool showLegend;
|
||||
|
||||
/// Format donut (anneau)
|
||||
final bool isDonut;
|
||||
|
||||
/// Rayon central pour le format donut (en pourcentage)
|
||||
final String innerRadius;
|
||||
|
||||
/// Activer l'effet 3D
|
||||
final bool enable3DEffect;
|
||||
|
||||
/// Intensité de l'effet 3D (1.0 = normal, 2.0 = fort)
|
||||
final double effect3DIntensity;
|
||||
|
||||
/// Activer l'effet d'explosion plus prononcé
|
||||
final bool enableEnhancedExplode;
|
||||
|
||||
/// Utiliser un dégradé pour simuler l'effet 3D
|
||||
final bool useGradient;
|
||||
|
||||
const PaymentPieChart({
|
||||
super.key,
|
||||
required this.payments,
|
||||
this.size = 300,
|
||||
this.labelSize = 12,
|
||||
this.showPercentage = true,
|
||||
this.showIcons = true,
|
||||
this.showLegend = true,
|
||||
this.isDonut = false,
|
||||
this.innerRadius = '40%',
|
||||
this.enable3DEffect = false,
|
||||
this.effect3DIntensity = 1.0,
|
||||
this.enableEnhancedExplode = false,
|
||||
this.useGradient = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PaymentPieChart> createState() => _PaymentPieChartState();
|
||||
}
|
||||
|
||||
class _PaymentPieChartState extends State<PaymentPieChart>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 2000),
|
||||
);
|
||||
|
||||
_animationController.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(PaymentPieChart oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
// Relancer l'animation si les données ont changé
|
||||
// Utiliser une comparaison plus stricte pour éviter des animations inutiles
|
||||
bool shouldResetAnimation = false;
|
||||
|
||||
if (oldWidget.payments.length != widget.payments.length) {
|
||||
shouldResetAnimation = true;
|
||||
} else {
|
||||
// Comparer les éléments importants uniquement
|
||||
for (int i = 0; i < oldWidget.payments.length; i++) {
|
||||
if (i >= widget.payments.length) break;
|
||||
if (oldWidget.payments[i].amount != widget.payments[i].amount ||
|
||||
oldWidget.payments[i].title != widget.payments[i].title) {
|
||||
shouldResetAnimation = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldResetAnimation) {
|
||||
_animationController.reset();
|
||||
_animationController.forward();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Prépare les données pour le graphique en camembert
|
||||
List<PaymentData> _prepareChartData() {
|
||||
// Filtrer les règlements avec un montant > 0
|
||||
return widget.payments.where((payment) => payment.amount > 0).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final chartData = _prepareChartData();
|
||||
|
||||
// Si aucune donnée, afficher un message
|
||||
if (chartData.isEmpty) {
|
||||
return SizedBox(
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
child: const Center(
|
||||
child: Text('Aucune donnée disponible'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Créer des animations pour différents aspects du graphique
|
||||
final progressAnimation = CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.easeOutCubic,
|
||||
);
|
||||
|
||||
final explodeAnimation = CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Interval(0.7, 1.0, curve: Curves.elasticOut),
|
||||
);
|
||||
|
||||
final opacityAnimation = CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Interval(0.1, 0.5, curve: Curves.easeIn),
|
||||
);
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return SizedBox(
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
child: SfCircularChart(
|
||||
margin: EdgeInsets.zero,
|
||||
legend: Legend(
|
||||
isVisible: widget.showLegend,
|
||||
position: LegendPosition.bottom,
|
||||
overflowMode: LegendItemOverflowMode.wrap,
|
||||
textStyle: TextStyle(fontSize: widget.labelSize),
|
||||
),
|
||||
tooltipBehavior: TooltipBehavior(enable: true),
|
||||
series: <CircularSeries>[
|
||||
widget.isDonut
|
||||
? DoughnutSeries<PaymentData, String>(
|
||||
dataSource: chartData,
|
||||
xValueMapper: (PaymentData data, _) => data.title,
|
||||
yValueMapper: (PaymentData data, _) => data.amount,
|
||||
pointColorMapper: (PaymentData data, _) {
|
||||
if (widget.enable3DEffect) {
|
||||
// Utiliser un angle différent pour chaque segment pour simuler un effet 3D
|
||||
final index = chartData.indexOf(data);
|
||||
final angle =
|
||||
(index / chartData.length) * 2 * math.pi;
|
||||
return widget.useGradient
|
||||
? _createEnhanced3DColor(data.color, angle)
|
||||
: _create3DColor(
|
||||
data.color, widget.effect3DIntensity);
|
||||
}
|
||||
return data.color;
|
||||
},
|
||||
// Note: Le gradient n'est pas directement pris en charge dans cette version de Syncfusion
|
||||
enableTooltip: true,
|
||||
dataLabelMapper: (PaymentData data, _) {
|
||||
if (widget.showPercentage) {
|
||||
// Calculer le pourcentage avec une décimale
|
||||
final total = chartData.fold(
|
||||
0.0, (sum, item) => sum + item.amount);
|
||||
final percentage = (data.amount / total * 100);
|
||||
return '${percentage.toStringAsFixed(1)}%';
|
||||
} else {
|
||||
return data.title;
|
||||
}
|
||||
},
|
||||
dataLabelSettings: DataLabelSettings(
|
||||
isVisible: true,
|
||||
labelPosition: ChartDataLabelPosition.outside,
|
||||
textStyle: TextStyle(fontSize: widget.labelSize),
|
||||
connectorLineSettings: const ConnectorLineSettings(
|
||||
type: ConnectorType.curve,
|
||||
length: '15%',
|
||||
),
|
||||
),
|
||||
innerRadius: widget.innerRadius,
|
||||
// Effet d'explosion plus prononcé pour donner du relief avec animation
|
||||
explode: true,
|
||||
explodeAll: widget.enableEnhancedExplode,
|
||||
explodeIndex: widget.enableEnhancedExplode ? null : 0,
|
||||
explodeOffset: widget.enableEnhancedExplode
|
||||
? widget.enable3DEffect
|
||||
? '${(12 * explodeAnimation.value).toStringAsFixed(1)}%'
|
||||
: '${(8 * explodeAnimation.value).toStringAsFixed(1)}%'
|
||||
: '${(5 * explodeAnimation.value).toStringAsFixed(1)}%',
|
||||
// Effet 3D via l'opacité et les couleurs avec animation
|
||||
opacity: widget.enable3DEffect
|
||||
? 0.95 * opacityAnimation.value
|
||||
: opacityAnimation.value,
|
||||
// Animation progressive du graphique
|
||||
animationDuration:
|
||||
0, // On désactive l'animation intégrée car nous utilisons notre propre animation
|
||||
startAngle: 270,
|
||||
endAngle: 270 + (360 * progressAnimation.value).toInt(),
|
||||
)
|
||||
: PieSeries<PaymentData, String>(
|
||||
dataSource: chartData,
|
||||
xValueMapper: (PaymentData data, _) => data.title,
|
||||
yValueMapper: (PaymentData data, _) => data.amount,
|
||||
pointColorMapper: (PaymentData data, _) {
|
||||
if (widget.enable3DEffect) {
|
||||
// Utiliser un angle différent pour chaque segment pour simuler un effet 3D
|
||||
final index = chartData.indexOf(data);
|
||||
final angle =
|
||||
(index / chartData.length) * 2 * math.pi;
|
||||
return widget.useGradient
|
||||
? _createEnhanced3DColor(data.color, angle)
|
||||
: _create3DColor(
|
||||
data.color, widget.effect3DIntensity);
|
||||
}
|
||||
return data.color;
|
||||
},
|
||||
// Note: Le gradient n'est pas directement pris en charge dans cette version de Syncfusion
|
||||
enableTooltip: true,
|
||||
dataLabelMapper: (PaymentData data, _) {
|
||||
if (widget.showPercentage) {
|
||||
// Calculer le pourcentage avec une décimale
|
||||
final total = chartData.fold(
|
||||
0.0, (sum, item) => sum + item.amount);
|
||||
final percentage = (data.amount / total * 100);
|
||||
return '${percentage.toStringAsFixed(1)}%';
|
||||
} else {
|
||||
return data.title;
|
||||
}
|
||||
},
|
||||
dataLabelSettings: DataLabelSettings(
|
||||
isVisible: true,
|
||||
labelPosition: ChartDataLabelPosition.outside,
|
||||
textStyle: TextStyle(fontSize: widget.labelSize),
|
||||
connectorLineSettings: const ConnectorLineSettings(
|
||||
type: ConnectorType.curve,
|
||||
length: '15%',
|
||||
),
|
||||
),
|
||||
// Effet d'explosion plus prononcé pour donner du relief avec animation
|
||||
explode: true,
|
||||
explodeAll: widget.enableEnhancedExplode,
|
||||
explodeIndex: widget.enableEnhancedExplode ? null : 0,
|
||||
explodeOffset: widget.enableEnhancedExplode
|
||||
? widget.enable3DEffect
|
||||
? '${(12 * explodeAnimation.value).toStringAsFixed(1)}%'
|
||||
: '${(8 * explodeAnimation.value).toStringAsFixed(1)}%'
|
||||
: '${(5 * explodeAnimation.value).toStringAsFixed(1)}%',
|
||||
// Effet 3D via l'opacité et les couleurs avec animation
|
||||
opacity: widget.enable3DEffect
|
||||
? 0.95 * opacityAnimation.value
|
||||
: opacityAnimation.value,
|
||||
// Animation progressive du graphique
|
||||
animationDuration:
|
||||
0, // On désactive l'animation intégrée car nous utilisons notre propre animation
|
||||
startAngle: 270,
|
||||
endAngle: 270 + (360 * progressAnimation.value).toInt(),
|
||||
),
|
||||
],
|
||||
annotations:
|
||||
widget.showIcons ? _buildIconAnnotations(chartData) : null,
|
||||
// Paramètres pour améliorer l'effet 3D
|
||||
palette: widget.enable3DEffect ? _create3DPalette(chartData) : null,
|
||||
// Ajouter un effet de bordure pour renforcer l'effet 3D
|
||||
borderWidth: widget.enable3DEffect ? 0.5 : 0,
|
||||
// Note: La rotation n'est pas directement prise en charge dans cette version de Syncfusion
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Crée une couleur avec effet 3D en ajoutant des nuances
|
||||
Color _create3DColor(Color baseColor, double intensity) {
|
||||
// Ajuster la luminosité et la saturation pour créer un effet 3D plus prononcé
|
||||
final hslColor = HSLColor.fromColor(baseColor);
|
||||
|
||||
// Augmenter la luminosité pour simuler un éclairage
|
||||
final adjustedLightness =
|
||||
(hslColor.lightness + 0.15 * intensity).clamp(0.0, 1.0);
|
||||
|
||||
// Augmenter légèrement la saturation pour des couleurs plus vives
|
||||
final adjustedSaturation =
|
||||
(hslColor.saturation + 0.05 * intensity).clamp(0.0, 1.0);
|
||||
|
||||
return hslColor
|
||||
.withLightness(adjustedLightness)
|
||||
.withSaturation(adjustedSaturation)
|
||||
.toColor();
|
||||
}
|
||||
|
||||
/// Crée une palette de couleurs pour l'effet 3D
|
||||
List<Color> _create3DPalette(List<PaymentData> chartData) {
|
||||
List<Color> palette = [];
|
||||
|
||||
// Créer des variations de couleurs pour chaque segment
|
||||
for (var i = 0; i < chartData.length; i++) {
|
||||
var data = chartData[i];
|
||||
|
||||
// Calculer un angle pour chaque segment pour simuler un éclairage directionnel
|
||||
final angle = (i / chartData.length) * 2 * math.pi;
|
||||
|
||||
// Créer un effet d'ombre et de lumière en fonction de l'angle
|
||||
final hslColor = HSLColor.fromColor(data.color);
|
||||
|
||||
// Ajuster la luminosité en fonction de l'angle
|
||||
final lightAdjustment = 0.15 * widget.effect3DIntensity * math.sin(angle);
|
||||
final adjustedLightness = (hslColor.lightness -
|
||||
0.1 * widget.effect3DIntensity +
|
||||
lightAdjustment)
|
||||
.clamp(0.0, 1.0);
|
||||
|
||||
// Ajuster la saturation pour plus de profondeur
|
||||
final adjustedSaturation =
|
||||
(hslColor.saturation + 0.1 * widget.effect3DIntensity)
|
||||
.clamp(0.0, 1.0);
|
||||
|
||||
final enhancedColor = hslColor
|
||||
.withLightness(adjustedLightness)
|
||||
.withSaturation(adjustedSaturation)
|
||||
.toColor();
|
||||
|
||||
palette.add(enhancedColor);
|
||||
}
|
||||
|
||||
return palette;
|
||||
}
|
||||
|
||||
/// Crée une couleur avec effet 3D plus avancé
|
||||
Color _createEnhanced3DColor(Color baseColor, double angle) {
|
||||
// Simuler un effet de lumière directionnel
|
||||
final hslColor = HSLColor.fromColor(baseColor);
|
||||
|
||||
// Ajuster la luminosité en fonction de l'angle pour simuler un éclairage
|
||||
final adjustedLightness = hslColor.lightness +
|
||||
(0.2 * widget.effect3DIntensity * math.sin(angle)).clamp(-0.3, 0.3);
|
||||
|
||||
return hslColor.withLightness(adjustedLightness.clamp(0.0, 1.0)).toColor();
|
||||
}
|
||||
|
||||
/// Crée les annotations d'icônes pour le graphique
|
||||
List<CircularChartAnnotation> _buildIconAnnotations(
|
||||
List<PaymentData> chartData) {
|
||||
final List<CircularChartAnnotation> annotations = [];
|
||||
|
||||
// Calculer le total pour les pourcentages
|
||||
double total = chartData.fold(0.0, (sum, item) => sum + item.amount);
|
||||
|
||||
// Position angulaire actuelle (en radians)
|
||||
double currentAngle = 0;
|
||||
|
||||
for (int i = 0; i < chartData.length; i++) {
|
||||
final data = chartData[i];
|
||||
final percentage = data.amount / total;
|
||||
|
||||
// Calculer l'angle central de ce segment
|
||||
final segmentAngle = percentage * 2 * 3.14159;
|
||||
final midAngle = currentAngle + (segmentAngle / 2);
|
||||
|
||||
// Ajouter une annotation pour l'icône
|
||||
annotations.add(
|
||||
CircularChartAnnotation(
|
||||
widget: Icon(
|
||||
data.icon,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
radius: '50%',
|
||||
angle: (midAngle * (180 / 3.14159)).toInt(), // Convertir en degrés
|
||||
),
|
||||
);
|
||||
|
||||
// Mettre à jour l'angle actuel
|
||||
currentAngle += segmentAngle;
|
||||
}
|
||||
|
||||
return annotations;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user