import 'package:currency_picker/currency_picker.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'; /// 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( (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: Theme.of(context).colorScheme.primary, 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: Theme.of(context).colorScheme.error, 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: Theme.of(context).colorScheme.primary, ), if (expenseData.isNotEmpty) BarChartRodData( toY: expenseData[index], color: Theme.of(context).colorScheme.error, ), ], ), ), ), ); }