import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:intl/intl.dart'; // For number formatting import '../models/category.dart'; // Keep using the Category model for UI structure // Changed to StatefulWidget to manage its own selection state class SpendingPieChart extends StatefulWidget { final List categories; // Expect List from database calculation final double totalExpenses; final Animation animation; // For fade/scale animation const SpendingPieChart({ Key? key, required this.categories, required this.totalExpenses, required this.animation, // Removed selectedPieIndex and onSelectPieCategory }) : super(key: key); @override State createState() => _SpendingPieChartState(); } class _SpendingPieChartState extends State { // State for the selected index is now managed internally int _selectedPieIndex = -1; // Handles selection logic internally void _handlePieTap(int index) { setState(() { // If the same index is selected, deselect (-1), otherwise select the new index _selectedPieIndex = (_selectedPieIndex == index) ? -1 : index; }); } @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; final theme = Theme.of(context); final currencyFormatter = NumberFormat.currency(locale: 'en_US', symbol: '\$'); // Or your preferred locale/symbol // Handle the case where there are no categories to display if (widget.categories.isEmpty) { return AnimatedBuilder( // Still use animation for consistency animation: widget.animation, builder: (context, child) => Opacity( opacity: widget.animation.value, child: Container( height: 230, // Maintain similar height to the chart version alignment: Alignment.center, padding: const EdgeInsets.all(16), child: Text( 'No spending data for this period.', textAlign: TextAlign.center, style: theme.textTheme.bodyMedium?.copyWith(color: Colors.grey), ), ), ), ); } // Use AnimatedBuilder to apply the fade/scale animation return AnimatedBuilder( animation: widget.animation, builder: (context, child) { return Transform.scale( scale: widget.animation.value, // Apply scale animation child: Opacity( opacity: widget.animation.value, // Apply fade animation child: Container( padding: const EdgeInsets.symmetric(vertical: 8.0), // Reduced vertical padding height: 230, // Fixed height for the chart and legend area child: Row( children: [ // --- Pie Chart --- Expanded( flex: 5, // Give more space to the chart itself child: Stack( // Use Stack to overlay text on the chart center alignment: Alignment.center, children: [ PieChart( PieChartData( // Handle touch events on the pie chart pieTouchData: PieTouchData( touchCallback: (FlTouchEvent event, PieTouchResponse? pieTouchResponse) { // We are only interested in TapUp events to trigger selection changes if (event is FlTapUpEvent) { final section = pieTouchResponse?.touchedSection; if (section != null) { // Tap occurred ON a section final touchedIndex = section.touchedSectionIndex; // Use internal handler to update state _handlePieTap(touchedIndex); } else { // Tap occurred OUTSIDE any section // Deselect if something was selected if (_selectedPieIndex != -1) { _handlePieTap(-1); // Pass -1 to deselect } } } }, ), borderData: FlBorderData(show: false), // No border around the chart sectionsSpace: 2, // Space between slices centerSpaceRadius: 50, // Radius of the center hole sections: _generatePieSections(context), // Generate slices data startDegreeOffset: -90, // Start chart from the top (12 o'clock) ), // Optional animation when data changes swapAnimationDuration: const Duration(milliseconds: 250), swapAnimationCurve: Curves.easeInOut, ), // --- Center Text (Displayed when a slice is selected) --- if (_selectedPieIndex != -1) _buildCenterText(context, currencyFormatter) else // Optional: Display total or default text when nothing is selected _buildDefaultCenterText(context, currencyFormatter), ], ), ), const SizedBox(width: 8), // Spacing between chart and legend // --- Legend --- Expanded( flex: 4, // Allocate space for the legend // Use ListView for scrollable legend if many categories child: ListView.builder( itemCount: widget.categories.length, padding: const EdgeInsets.only(right: 8), // Padding for legend items itemBuilder: (context, index) => _buildPieLegendItem(context, index), ), ), ], ), ), ), ); }, ); } // Builds the text displayed in the center when a slice is selected Widget _buildCenterText(BuildContext context, NumberFormat formatter) { final theme = Theme.of(context); // Check if selectedPieIndex is valid before accessing categories if (_selectedPieIndex < 0 || _selectedPieIndex >= widget.categories.length) { // Return an empty container or default text if index is invalid return _buildDefaultCenterText(context, formatter); } final selectedCategory = widget.categories[_selectedPieIndex]; return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( selectedCategory.name, style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.bold, color: theme.textTheme.bodyLarge?.color?.withOpacity(0.8), ), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text( formatter.format(selectedCategory.amount), // Format the amount as currency style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: selectedCategory.colorCode, // Use category color for amount ), textAlign: TextAlign.center, ), ], ); } // Builds the default text displayed in the center when no slice is selected Widget _buildDefaultCenterText(BuildContext context, NumberFormat formatter) { final theme = Theme.of(context); return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Total', // Label for the total amount style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.bold, color: theme.textTheme.bodyLarge?.color?.withOpacity(0.7), ), textAlign: TextAlign.center, ), const SizedBox(height: 4), Text( formatter.format(widget.totalExpenses), // Display total expenses style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: theme.textTheme.bodyLarge?.color // Use default text color ), textAlign: TextAlign.center, ), ], ); } // Builds a single item for the legend Widget _buildPieLegendItem(BuildContext context, int index) { final isDark = Theme.of(context).brightness == Brightness.dark; final theme = Theme.of(context); final isSelected = index == _selectedPieIndex; // Check if this item is selected using internal state // Check if index is valid before accessing categories if (index < 0 || index >= widget.categories.length) { return const SizedBox.shrink(); // Return empty if index is invalid } final category = widget.categories[index]; // Calculate percentage, handle totalExpenses being zero final percentage = widget.totalExpenses > 0 ? (category.amount / widget.totalExpenses * 100) : 0.0; // Use InkWell for tap feedback and GestureDetector for tap logic return InkWell( onTap: () => _handlePieTap(index), // Use internal handler on tap borderRadius: BorderRadius.circular(8), // Match border radius child: AnimatedContainer( duration: const Duration(milliseconds: 200), // Animation for selection change margin: const EdgeInsets.symmetric(vertical: 3.0), // Spacing between legend items padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 6.0), // Padding inside item decoration: BoxDecoration( // Highlight background if selected color: isSelected ? category.colorCode.withOpacity(isDark ? 0.3 : 0.15) : Colors.transparent, borderRadius: BorderRadius.circular(8), // Add border if selected border: Border.all( color: isSelected ? category.colorCode.withOpacity(0.8) : Colors.transparent, width: 1.5, ), ), child: Row( children: [ // Color indicator dot Container( width: 10, height: 10, decoration: BoxDecoration( shape: BoxShape.circle, color: category.colorCode.withOpacity(isDark ? 0.9 : 1.0), // Use category color ), ), const SizedBox(width: 8), // Spacing // Category name Expanded( child: Text( category.name, style: theme.textTheme.bodyMedium?.copyWith( fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, // Bold if selected color: theme.textTheme.bodyLarge?.color, // Use default body text color ), overflow: TextOverflow.ellipsis, // Prevent long names from wrapping ), ), const SizedBox(width: 8), // Spacing // Percentage text Text( '${percentage.toStringAsFixed(1)}%', // Format percentage style: theme.textTheme.bodySmall?.copyWith( fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, // Bold if selected color: theme.textTheme.bodyMedium?.color?.withOpacity(0.7), // Slightly faded color ), ), ], ), ), ); } // Generates the data for each slice (PieChartSectionData) of the pie chart List _generatePieSections(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return List.generate(widget.categories.length, (i) { // Check if index is valid before accessing categories if (i < 0 || i >= widget.categories.length) { // This should ideally not happen if List.generate is used correctly, // but adding a safeguard. return PieChartSectionData(); // Return an empty section } final isTouched = i == _selectedPieIndex; // Check if this slice is selected using internal state // Make selected slice slightly larger final double radius = isTouched ? 65 : 55; // Make title font slightly larger when selected final double titleFontSize = isTouched ? 14 : 12; final category = widget.categories[i]; // Calculate percentage for the title final percentage = widget.totalExpenses > 0 ? (category.amount / widget.totalExpenses * 100) : 0; return PieChartSectionData( color: category.colorCode.withOpacity(isDark ? 0.85 : 1.0), // Use category color value: category.amount, // Value determines the slice size title: '${percentage.toStringAsFixed(0)}%', // Display percentage as title radius: radius, // Apply radius (larger if touched) titleStyle: TextStyle( fontSize: titleFontSize, // Apply font size (larger if touched) fontWeight: FontWeight.bold, color: Colors.white.withOpacity(0.9), // White text for contrast on colored slices shadows: const [Shadow(color: Colors.black38, blurRadius: 2)], // Subtle shadow for readability ), // Add border to selected slice for emphasis borderSide: isTouched ? BorderSide(color: isDark ? Colors.white60 : Colors.black54, width: 2) : BorderSide(color: category.colorCode.withOpacity(0.5), width: 1), // Optional: Add badge (icon) to the selected slice // badgeWidget: isTouched ? Icon(category.icon, color: Colors.white, size: 16) : null, // badgePositionPercentageOffset: .98, ); }); } }