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:intl/intl.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:prasule/api/category.dart'; import 'package:prasule/api/walletentry.dart'; /// Monthly/Yearly expense/income [LineChart] class ExpensesLineChart extends StatelessWidget { const ExpensesLineChart( {super.key, required this.date, required this.locale, required this.expenseData, required this.incomeData, required this.currency, this.yearly = false}); final bool yearly; final DateTime date; final String locale; final List expenseData; final Currency currency; List get expenseDataSorted { var list = List.from(expenseData); list.sort((a, b) => a.compareTo(b)); return list; } final List incomeData; List get incomeDataSorted { var list = List.from(incomeData); list.sort((a, b) => a.compareTo(b)); return list; } 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, 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, 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(show: false), 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(show: false), 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( sideTitles: SideTitles(showTitles: false), ), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), 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, 1), ); } else { text = (value.toInt() + 1).toString(); } return SideTitleWidget( axisSide: meta.axisSide, child: Text(text)); }, ), ), ), // axis descriptions ), ); } } class ExpensesBarChart extends StatelessWidget { const ExpensesBarChart( {super.key, required this.yearly, required this.date, required this.locale, required this.expenseData, required this.incomeData, required this.currency}); final bool yearly; final DateTime date; final String locale; final List expenseData; List get expenseDataSorted { var list = List.from(expenseData); list.sort((a, b) => a.compareTo(b)); return list; } final Currency currency; final List incomeData; List get incomeDataSorted { var list = List.from(incomeData); list.sort((a, b) => a.compareTo(b)); return list; } 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, 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, 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( sideTitles: SideTitles(showTitles: false), ), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), 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, 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, ), ), ), ), ); }