import 'package:currency_picker/currency_picker.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:prasule/api/category.dart'; import 'package:prasule/api/walletentry.dart'; import 'package:intl/intl.dart'; /// Monthly/Yearly expense/income [LineChart] class ExpensesLineChart extends StatelessWidget { /// Monthly/Yearly expense/income [LineChart] const ExpensesLineChart({ required this.date, required this.locale, required this.expenseData, required this.incomeData, required this.currency, super.key, this.yearly = false, }); /// If the graph will be shown yearly final bool yearly; /// Selected date /// /// Used to get either month or year final DateTime date; /// Current locale /// /// Used mainly for formatting final String locale; /// The expense data used for the graph final List expenseData; /// Wallet currency /// /// Used to show currency symbol final Currency currency; /// Expense data, but sorted List get expenseDataSorted => List.from(expenseData)..sort((a, b) => a.compareTo(b)); /// Income data used for the graph final List incomeData; /// Income data, but sorted List get incomeDataSorted => List.from(incomeData)..sort((a, b) => a.compareTo(b)); /// Calculates maxY for the graph double get maxY { if (incomeData.isEmpty) return expenseDataSorted.last; if (expenseData.isEmpty) return incomeDataSorted.last; if (expenseDataSorted.last > incomeDataSorted.last) { return expenseDataSorted.last; } else { return incomeDataSorted.last; } } @override Widget build(BuildContext context) { return LineChart( LineChartData( lineTouchData: LineTouchData( touchTooltipData: LineTouchTooltipData( getTooltipItems: (spots) => List.generate( spots.length, (index) => LineTooltipItem( (spots[index].barIndex == 0) ? (yearly ? AppLocalizations.of(context).incomeForMonth( DateFormat.MMMM(locale).format( DateTime( date.year, spots[index].x.toInt() + 1, ), ), NumberFormat.compactCurrency( locale: locale, symbol: currency.symbol, name: currency.name, ).format(spots[index].y), ) : AppLocalizations.of(context).incomeForDay( NumberFormat.compactCurrency( locale: locale, symbol: currency.symbol, name: currency.name, ).format(spots[index].y), )) : (yearly ? AppLocalizations.of(context).expensesForMonth( DateFormat.MMMM(locale).format( DateTime( date.year, spots[index].x.toInt() + 1, ), ), NumberFormat.compactCurrency( locale: locale, symbol: currency.symbol, name: currency.name, ).format(spots[index].y), ) : AppLocalizations.of(context).expensesForDay( NumberFormat.compactCurrency( locale: locale, symbol: currency.symbol, name: currency.name, ).format(spots[index].y), )), TextStyle(color: spots[index].bar.color), ), ), ), ), maxY: maxY, maxX: yearly ? 12 : DateTime(date.year, date.month, 0).day.toDouble(), minY: 0, minX: 0, backgroundColor: Theme.of(context).colorScheme.background, lineBarsData: [ if (incomeData.isNotEmpty) LineChartBarData( isCurved: true, barWidth: 8, isStrokeCapRound: true, dotData: const FlDotData(show: false), belowBarData: BarAreaData(), color: Colors.green .harmonizeWith(Theme.of(context).colorScheme.secondary), spots: List.generate( yearly ? 12 : DateTime(date.year, date.month, 0).day, (index) => FlSpot(index.toDouble(), incomeData[index]), ), ), if (expenseData.isNotEmpty) LineChartBarData( isCurved: true, barWidth: 8, isStrokeCapRound: true, dotData: const FlDotData(show: false), belowBarData: BarAreaData(), color: Colors.red .harmonizeWith(Theme.of(context).colorScheme.secondary), spots: List.generate( yearly ? 12 : DateTime(date.year, date.month, 0).day, (index) => FlSpot(index.toDouble() + 1, expenseData[index]), ), ), ], // actual data titlesData: FlTitlesData( rightTitles: const AxisTitles(), topTitles: const AxisTitles(), bottomTitles: AxisTitles( sideTitles: SideTitles( reservedSize: 30, showTitles: true, getTitlesWidget: (value, meta) { String text; if (yearly) { text = DateFormat.MMM(locale).format( DateTime(date.year, value.toInt() + 1), ); } else { text = (value.toInt() + 1).toString(); } return SideTitleWidget( axisSide: meta.axisSide, child: Text(text), ); }, ), ), ), // axis descriptions ), ); } } /// Renders expenses/income as a [BarChart] class ExpensesBarChart extends StatelessWidget { /// Renders expenses/income as a [BarChart] const ExpensesBarChart({ required this.yearly, required this.date, required this.locale, required this.expenseData, required this.incomeData, required this.currency, super.key, }); /// If the graph will be shown yearly final bool yearly; /// Selected date /// /// Used to get either month or year final DateTime date; /// Current locale /// /// Used mainly for formatting final String locale; /// The expense data used for the graph final List expenseData; /// Wallet currency /// /// Used to show currency symbol final Currency currency; /// Expense data, but sorted List get expenseDataSorted => List.from(expenseData)..sort((a, b) => a.compareTo(b)); /// Income data used for the graph final List incomeData; /// Income data, but sorted List get incomeDataSorted => List.from(incomeData)..sort((a, b) => a.compareTo(b)); /// Calculates maxY for the graph double get maxY { if (incomeData.isEmpty) return expenseDataSorted.last; if (expenseData.isEmpty) return incomeDataSorted.last; if (expenseDataSorted.last > incomeDataSorted.last) { return expenseDataSorted.last; } else { return incomeDataSorted.last; } } @override Widget build(BuildContext context) => BarChart( BarChartData( barTouchData: BarTouchData( enabled: true, touchTooltipData: BarTouchTooltipData( getTooltipItem: (group, groupIndex, rod, rodIndex) => yearly // create custom tooltips for graph bars ? BarTooltipItem( (rodIndex == 1) ? AppLocalizations.of(context).expensesForMonth( DateFormat.MMMM(locale).format( DateTime(date.year, groupIndex + 1), ), NumberFormat.compactCurrency( locale: locale, symbol: currency.symbol, name: currency.name, ).format(rod.toY), ) : AppLocalizations.of(context).incomeForMonth( DateFormat.MMMM(locale).format( DateTime(date.year, groupIndex + 1), ), NumberFormat.compactCurrency( locale: locale, symbol: currency.symbol, name: currency.name, ).format(rod.toY), ), TextStyle(color: rod.color), ) : BarTooltipItem( (rodIndex == 1) ? AppLocalizations.of(context).expensesForDay( NumberFormat.compactCurrency( locale: locale, symbol: currency.symbol, name: currency.name, ).format(rod.toY), ) : AppLocalizations.of(context).incomeForDay( NumberFormat.compactCurrency( locale: locale, symbol: currency.symbol, name: currency.name, ).format(rod.toY), ), TextStyle(color: rod.color), ), ), ), titlesData: FlTitlesData( rightTitles: const AxisTitles(), topTitles: const AxisTitles(), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 30, getTitlesWidget: (value, meta) { String text; if (yearly) { text = DateFormat.MMM(locale).format( DateTime(date.year, value.toInt() + 1), ); } else { text = (value.toInt() + 1).toString(); } return SideTitleWidget( axisSide: meta.axisSide, child: Text(text), ); }, ), ), ), // axis descriptions, minY: 0, maxY: maxY, barGroups: List.generate( yearly ? 12 : DateTime(date.year, date.month, 0).day, (index) => BarChartGroupData( x: index, barRods: [ if (incomeData.isNotEmpty) BarChartRodData( toY: incomeData[index], color: Colors.green .harmonizeWith(Theme.of(context).colorScheme.secondary), ), if (expenseData.isNotEmpty) BarChartRodData( toY: expenseData[index], color: Colors.red .harmonizeWith(Theme.of(context).colorScheme.secondary), ), ], ), ), ), ); } class CategoriesPieChart extends StatelessWidget { const CategoriesPieChart( {super.key, required this.entries, required this.categories}); final List entries; final List categories; @override Widget build(BuildContext context) => PieChart( PieChartData( sections: List.generate( categories.length, (index) => PieChartSectionData( value: entries .where( (element) => element.category.id == categories[index].id) .fold( 0, (previousValue, element) => previousValue + element.data.amount, ), ), ), ), ); }