From a51ca140418988951974a3e768662a6daf0b9edf Mon Sep 17 00:00:00 2001 From: gltron Date: Sun, 11 Feb 2024 22:49:57 +0100 Subject: [PATCH] Improved stacked graph --- lib/domains/charts/chart_bloc.dart | 26 +++++--- lib/domains/charts/chart_state.dart | 14 ++++- lib/domains/charts/models/month_totals.dart | 29 +++++++++ .../widgets/categories_totals_chart.dart | 10 +-- .../monthly_categories_total_chart.dart | 63 ++++++++++++++++--- 5 files changed, 118 insertions(+), 24 deletions(-) create mode 100644 lib/domains/charts/models/month_totals.dart diff --git a/lib/domains/charts/chart_bloc.dart b/lib/domains/charts/chart_bloc.dart index 75c3ded..9adc9f4 100644 --- a/lib/domains/charts/chart_bloc.dart +++ b/lib/domains/charts/chart_bloc.dart @@ -1,6 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tunas/domains/charts/models/month_totals.dart'; import 'package:tunas/domains/transaction/models/transaction_line.dart'; import 'package:tunas/domains/charts/models/chart_item.dart'; import 'package:tunas/repositories/account/account_repository.dart'; @@ -100,7 +101,10 @@ class ChartBloc extends Bloc { List scopedCategoriesPositiveTotals = []; List scopedCategoriesNegativeTotals = []; Map scopedMonthlyTotals = {}; - Map> scopedCategoriesMonthlyTotals = {}; + Map scopedCategoriesMonthlyTotals = {}; + + Map scopedMonthlyPostitiveTotals = {}; + Map scopedMonthlyNegativeTotals = {}; for(var transaction in state.transactions) { globalTotal += transaction.value; @@ -123,6 +127,12 @@ class ChartBloc extends Bloc { continue; } + MonthTotals? categoryMonthTotal = scopedCategoriesMonthlyTotals[transaction.date.month]; + if (categoryMonthTotal == null) { + categoryMonthTotal = MonthTotals(negatives: {}, positives: {}); + scopedCategoriesMonthlyTotals[transaction.date.month] = categoryMonthTotal; + } + if (transaction.value >= 0) { ChartItem? chartItem = scopedCategoriesPositiveTotals.firstWhere( (item) => item.label == transaction.category, @@ -133,6 +143,9 @@ class ChartBloc extends Bloc { } ); chartItem.value += transaction.value; + + scopedMonthlyPostitiveTotals[transaction.date.month] = transaction.value + (scopedMonthlyPostitiveTotals[transaction.date.month] ?? 0); + categoryMonthTotal.positives[transaction.category] = transaction.value.abs() + (categoryMonthTotal.positives[transaction.category] ?? 0); } if (transaction.value < 0) { @@ -146,13 +159,8 @@ class ChartBloc extends Bloc { ); chartItem.value += transaction.value; - Map? a = scopedCategoriesMonthlyTotals[transaction.date.month]; - if (scopedCategoriesMonthlyTotals[transaction.date.month] == null) { - a = {}; - } - - a?[transaction.category] = transaction.value.abs() + (a[transaction.category] ?? 0); - scopedCategoriesMonthlyTotals[transaction.date.month] = a!; + scopedMonthlyNegativeTotals[transaction.date.month] = transaction.value + (scopedMonthlyPostitiveTotals[transaction.date.month] ?? 0); + categoryMonthTotal.negatives[transaction.category] = transaction.value.abs() + (categoryMonthTotal.negatives[transaction.category] ?? 0); } } @@ -214,6 +222,8 @@ class ChartBloc extends Bloc { scopedSimplifiedCategoriesNegativeTotalsPercents: scopedSimplifiedCategoriesNegativeTotalsPercents, scopedMonthlyTotals: scopedMonthlyTotals.values.toList(), scopedCategoriesMonthlyTotals: scopedCategoriesMonthlyTotals, + scopedMonthlyPostitiveTotals: scopedMonthlyPostitiveTotals, + scopedMonthlyNegativeTotals: scopedMonthlyNegativeTotals, ); } } diff --git a/lib/domains/charts/chart_state.dart b/lib/domains/charts/chart_state.dart index 23a6042..70ad345 100644 --- a/lib/domains/charts/chart_state.dart +++ b/lib/domains/charts/chart_state.dart @@ -27,7 +27,9 @@ final class ChartState extends Equatable { final List scopedSimplifiedCategoriesNegativeTotalsPercents; final List scopedMonthlyTotals; - final Map> scopedCategoriesMonthlyTotals; + final Map scopedCategoriesMonthlyTotals; + final Map scopedMonthlyPostitiveTotals; + final Map scopedMonthlyNegativeTotals; final double scoppedProfit; @@ -53,6 +55,8 @@ final class ChartState extends Equatable { this.scopedMonthlyTotals = const [], this.scopedCategoriesMonthlyTotals = const {}, this.scoppedProfit = 0, + this.scopedMonthlyPostitiveTotals = const {}, + this.scopedMonthlyNegativeTotals = const {}, }); ChartState copyWith({ @@ -75,8 +79,10 @@ final class ChartState extends Equatable { List? scopedSimplifiedCategoriesNegativeTotals, List? scopedSimplifiedCategoriesNegativeTotalsPercents, List? scopedMonthlyTotals, - Map>? scopedCategoriesMonthlyTotals, + Map? scopedCategoriesMonthlyTotals, double? scoppedProfit, + Map? scopedMonthlyPostitiveTotals, + Map? scopedMonthlyNegativeTotals, }) { return ChartState( transactions: transactions ?? this.transactions, @@ -100,6 +106,8 @@ final class ChartState extends Equatable { scopedMonthlyTotals: scopedMonthlyTotals ?? this.scopedMonthlyTotals, scopedCategoriesMonthlyTotals: scopedCategoriesMonthlyTotals ?? this.scopedCategoriesMonthlyTotals, scoppedProfit: scoppedProfit ?? this.scoppedProfit, + scopedMonthlyPostitiveTotals: scopedMonthlyPostitiveTotals ?? this.scopedMonthlyPostitiveTotals, + scopedMonthlyNegativeTotals: scopedMonthlyNegativeTotals ?? this.scopedMonthlyNegativeTotals, ); } @@ -124,6 +132,8 @@ final class ChartState extends Equatable { scopedMonthlyTotals, scopedCategoriesMonthlyTotals, scoppedProfit, + scopedMonthlyPostitiveTotals, + scopedMonthlyNegativeTotals, ]; } diff --git a/lib/domains/charts/models/month_totals.dart b/lib/domains/charts/models/month_totals.dart new file mode 100644 index 0000000..2831b96 --- /dev/null +++ b/lib/domains/charts/models/month_totals.dart @@ -0,0 +1,29 @@ +class MonthTotals { + Map positives; + Map negatives; + + MonthTotals({ + required this.positives, + required this.negatives, + }); + + double maxValue() { + double max = 0.0; + + if (positives.isNotEmpty) { + double localMax = positives.values.reduce((value, element) => value + element); + if (localMax > max) { + max = localMax; + } + } + + if (negatives.isNotEmpty) { + double localMax2 = negatives.values.reduce((value, element) => value + element); + if (localMax2 > max) { + max = localMax2; + } + } + + return max; + } +} \ No newline at end of file diff --git a/lib/pages/stats/widgets/categories_totals_chart.dart b/lib/pages/stats/widgets/categories_totals_chart.dart index 6ad0815..a05df11 100644 --- a/lib/pages/stats/widgets/categories_totals_chart.dart +++ b/lib/pages/stats/widgets/categories_totals_chart.dart @@ -21,9 +21,9 @@ class CategoriesTotalsChart extends StatelessWidget { fontSize: 15, fontWeight: FontWeight.w300 ), - titlePositionPercentageOffset: 0.8, + titlePositionPercentageOffset: 0.5, borderSide: const BorderSide(width: 0), - radius: 150, + radius: 40, color: categoriesColors[item.label] )) .toList(); @@ -60,7 +60,7 @@ class CategoriesTotalsChart extends StatelessWidget { return BlocBuilder( builder: (context, state) => Container( height: 320, - width: 600, + width: 500, padding: const EdgeInsets.all(10), margin: const EdgeInsets.all(20), decoration: BoxDecoration( @@ -76,8 +76,8 @@ class CategoriesTotalsChart extends StatelessWidget { borderData: FlBorderData( show: false ), - centerSpaceRadius: 0, - sectionsSpace: 2 + centerSpaceRadius: 50, + sectionsSpace: 4 ) ), ), diff --git a/lib/pages/stats/widgets/monthly_categories_total_chart.dart b/lib/pages/stats/widgets/monthly_categories_total_chart.dart index 0df4ae1..834f20a 100644 --- a/lib/pages/stats/widgets/monthly_categories_total_chart.dart +++ b/lib/pages/stats/widgets/monthly_categories_total_chart.dart @@ -1,22 +1,28 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; import 'package:tunas/domains/category/category_bloc.dart'; +import 'package:tunas/domains/charts/models/month_totals.dart'; class MonthlyCategoriesTotalChart extends StatelessWidget { - final Map> categoriesMonthlyTotals; + final Map categoriesMonthlyTotals; const MonthlyCategoriesTotalChart({super.key, required this.categoriesMonthlyTotals}); - BarChartRodData _computeStack(double barsWidth, MapEntry> entry, Map categoriesColors) { + BarChartRodData _computeStack(double barsWidth, Map entry, Map categoriesColors) { var subcounter = 0.0; - var a = entry.value.entries.map((subEntry) => BarChartRodStackItem(subcounter, subcounter += subEntry.value, categoriesColors[subEntry.key] ?? Colors.red)).toList(); + var items = entry.entries.map((subEntry) => BarChartRodStackItem( + subcounter, subcounter += subEntry.value, + categoriesColors[subEntry.key] ?? Colors.red, + )).toList(); return BarChartRodData( + color: Colors.transparent, fromY: 0, toY: subcounter, width: barsWidth, - borderRadius: BorderRadius.zero, - rodStackItems: a + borderRadius: BorderRadius.circular(3), + rodStackItems: items ); } @@ -26,7 +32,11 @@ class MonthlyCategoriesTotalChart extends StatelessWidget { return BarChartGroupData( x: entry.key, barsSpace: barsSpace, - barRods: [_computeStack(barsWidth, entry, categoriesColors)] + barRods: [ + _computeStack(barsWidth, entry.value.positives, categoriesColors), + _computeStack(barsWidth, entry.value.negatives, categoriesColors), + ], + showingTooltipIndicators: [0, 1] ); }) .toList(); @@ -46,7 +56,7 @@ class MonthlyCategoriesTotalChart extends StatelessWidget { double _computeMaxValue() { double max = 0.0; categoriesMonthlyTotals.forEach((monthKey, value) { - double localMax = value.values.reduce((value, element) => value + element); + double localMax = value.maxValue(); if (localMax > max) { max = localMax; } @@ -61,8 +71,8 @@ class MonthlyCategoriesTotalChart extends StatelessWidget { aspectRatio: 1.66, child: LayoutBuilder( builder: (context, constraints) { - final barsSpace = 4.0 * constraints.maxWidth / 100; - final barsWidth = 8.0 * constraints.maxWidth / 130; + final barsSpace = 4.0 * constraints.maxWidth / 300; + final barsWidth = 8.0 * constraints.maxWidth / 500; return BarChart( BarChartData( @@ -78,6 +88,41 @@ class MonthlyCategoriesTotalChart extends StatelessWidget { getTitlesWidget: _computeBottomTitles ) ) + ), + barTouchData: BarTouchData( + enabled: true, + handleBuiltInTouches: false, + touchTooltipData: BarTouchTooltipData( + tooltipBgColor: Colors.transparent, + tooltipMargin: 0, + getTooltipItem:(group, groupIndex, rod, rodIndex) { + String value = NumberFormat("#00").format(rod.toY); + Color color = Colors.black; + if (rodIndex == 0) { + value = "+$value"; + color = Colors.green; + } else { + value = "-$value"; + color = Colors.red; + } + + return BarTooltipItem( + value, + TextStyle( + fontWeight: FontWeight.bold, + color: color, + fontSize: 12, + fontFamily: 'NovaMono', + shadows: const [ + Shadow( + color: Colors.black26, + blurRadius: 12, + ) + ], + ), + ); + } + ) ) ) );