feat: display category/total amount in pie chart center

This commit is contained in:
2025-05-04 14:37:36 +03:00
parent dca892c8a0
commit 034c405a29
+98 -30
View File
@@ -24,6 +24,7 @@ class SpendingPieChart extends StatelessWidget {
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 (categories.isEmpty) {
@@ -61,38 +62,48 @@ class SpendingPieChart extends StatelessWidget {
// --- Pie Chart ---
Expanded(
flex: 5, // Give more space to the chart itself
child: PieChart(
PieChartData(
// Handle touch events on the pie chart
pieTouchData: PieTouchData(
touchCallback: (FlTouchEvent event, pieTouchResponse) {
// Ignore events not related to interaction
if (!event.isInterestedForInteractions ||
pieTouchResponse == null ||
pieTouchResponse.touchedSection == null) {
// If touch ends outside a section, deselect
if (event is FlPanEndEvent || event is FlTapUpEvent) {
if (selectedPieIndex != -1) {
onSelectPieCategory(-1); // Deselect
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) {
// Ignore events not related to interaction
if (!event.isInterestedForInteractions ||
pieTouchResponse == null ||
pieTouchResponse.touchedSection == null) {
// If touch ends outside a section, deselect
if (event is FlPanEndEvent || event is FlTapUpEvent) {
if (selectedPieIndex != -1) {
onSelectPieCategory(-1); // Deselect
}
}
return;
}
}
return;
}
// Get the index of the touched section
final touchedIndex = pieTouchResponse.touchedSection!.touchedSectionIndex;
// Call the callback, toggling selection if the same slice is touched again
onSelectPieCategory(touchedIndex == selectedPieIndex ? -1 : touchedIndex);
},
// Get the index of the touched section
final touchedIndex = pieTouchResponse.touchedSection!.touchedSectionIndex;
// Call the callback, toggling selection if the same slice is touched again
onSelectPieCategory(touchedIndex == selectedPieIndex ? -1 : touchedIndex);
},
),
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,
),
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
@@ -116,6 +127,63 @@ class SpendingPieChart extends StatelessWidget {
);
}
// Builds the text displayed in the center when a slice is selected
Widget _buildCenterText(BuildContext context, NumberFormat formatter) {
final theme = Theme.of(context);
final selectedCategory = 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(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;