import 'dart:math'; import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import '../tokens/unionflow_colors.dart'; /// Graphique en ligne UnionFlow - Pour afficher l'évolution temporelle class UnionLineChart extends StatelessWidget { final List spots; final String title; final String? subtitle; final Color? lineColor; final Color? gradientStartColor; final Color? gradientEndColor; const UnionLineChart({ super.key, required this.spots, required this.title, this.subtitle, this.lineColor, this.gradientStartColor, this.gradientEndColor, }); /// Calcule maxY de manière sécurisée pour éviter NaN, Infinity ou 0 double _calculateSafeMaxY() { if (spots.isEmpty) return 100.0; final maxValue = spots.map((e) => e.y).reduce((a, b) => a > b ? a : b); // Si maxValue est invalide (NaN, Infinity) ou trop petit if (maxValue.isNaN || maxValue.isInfinite || maxValue <= 0) { return 100.0; } return maxValue * 1.2; } /// Calcule maxX de manière sécurisée double _calculateSafeMaxX() { if (spots.isEmpty) return 11.0; // 12 mois - 1 return spots.length.toDouble() - 1; } /// Calcule l'intervalle de grille approprié basé sur maxY double _calculateGridInterval() { final maxY = _calculateSafeMaxY(); // Calculer un intervalle qui donne environ 4-6 lignes de grille final baseInterval = maxY / 5; if (baseInterval == 0) return 20.0; // Fallback si maxY est trop petit // Arrondir à un nombre "propre" (puissance de 10) final magnitude = pow(10.0, (log(baseInterval) / log(10.0)).floor()).toDouble(); final normalized = baseInterval / magnitude; // Arrondir vers le haut au multiple de 1, 2 ou 5 le plus proche double roundedInterval; if (normalized <= 1) { roundedInterval = 1; } else if (normalized <= 2) { roundedInterval = 2; } else if (normalized <= 5) { roundedInterval = 5; } else { roundedInterval = 10; } return roundedInterval * magnitude; } @override Widget build(BuildContext context) { final effectiveLineColor = lineColor ?? UnionFlowColors.unionGreen; final effectiveGradientStart = gradientStartColor ?? UnionFlowColors.unionGreen.withOpacity(0.3); final effectiveGradientEnd = gradientEndColor ?? UnionFlowColors.unionGreen.withOpacity(0.0); return Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: UnionFlowColors.surface, borderRadius: BorderRadius.circular(10), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header Text( title, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: UnionFlowColors.textPrimary, ), ), if (subtitle != null) ...[ const SizedBox(height: 4), Text( subtitle!, style: const TextStyle( fontSize: 11, color: UnionFlowColors.textSecondary, ), ), ], const SizedBox(height: 10), // Chart SizedBox( height: 180, child: LineChart( LineChartData( gridData: FlGridData( show: true, drawVerticalLine: false, horizontalInterval: _calculateGridInterval(), getDrawingHorizontalLine: (value) { return FlLine( color: UnionFlowColors.border.withOpacity(0.2), strokeWidth: 1, ); }, ), titlesData: FlTitlesData( show: true, rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 30, interval: 1, getTitlesWidget: (value, meta) { const months = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun', 'Jul', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc']; if (value.toInt() >= 0 && value.toInt() < months.length) { return Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( months[value.toInt()], style: const TextStyle( fontSize: 10, color: UnionFlowColors.textTertiary, ), ), ); } return const Text(''); }, ), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 40, getTitlesWidget: (value, meta) { return Text( '${(value / 1000).toStringAsFixed(0)}K', style: const TextStyle( fontSize: 10, color: UnionFlowColors.textTertiary, ), ); }, ), ), ), borderData: FlBorderData(show: false), minX: 0, maxX: _calculateSafeMaxX(), minY: 0, maxY: _calculateSafeMaxY(), lineBarsData: [ LineChartBarData( spots: spots, isCurved: true, color: effectiveLineColor, barWidth: 3, isStrokeCapRound: true, dotData: FlDotData( show: true, getDotPainter: (spot, percent, barData, index) { return FlDotCirclePainter( radius: 4, color: UnionFlowColors.surface, strokeWidth: 2, strokeColor: effectiveLineColor, ); }, ), belowBarData: BarAreaData( show: true, gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ effectiveGradientStart, effectiveGradientEnd, ], ), ), ), ], ), ), ), ], ), ); } }