370 lines
13 KiB
Dart
370 lines
13 KiB
Dart
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<double> expenseData;
|
|
|
|
/// Wallet currency
|
|
///
|
|
/// Used to show currency symbol
|
|
final Currency currency;
|
|
|
|
/// Expense data, but sorted
|
|
List<double> get expenseDataSorted =>
|
|
List<double>.from(expenseData)..sort((a, b) => a.compareTo(b));
|
|
|
|
/// Income data used for the graph
|
|
final List<double> incomeData;
|
|
|
|
/// Income data, but sorted
|
|
List<double> get incomeDataSorted =>
|
|
List<double>.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<LineTooltipItem>.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<double> expenseData;
|
|
|
|
/// Wallet currency
|
|
///
|
|
/// Used to show currency symbol
|
|
final Currency currency;
|
|
|
|
/// Expense data, but sorted
|
|
List<double> get expenseDataSorted =>
|
|
List<double>.from(expenseData)..sort((a, b) => a.compareTo(b));
|
|
|
|
/// Income data used for the graph
|
|
final List<double> incomeData;
|
|
|
|
/// Income data, but sorted
|
|
List<double> get incomeDataSorted =>
|
|
List<double>.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<BarChartGroupData>.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<WalletSingleEntry> entries;
|
|
final List<WalletCategory> categories;
|
|
|
|
@override
|
|
Widget build(BuildContext context) => PieChart(
|
|
PieChartData(
|
|
sections: List<PieChartSectionData>.generate(
|
|
categories.length,
|
|
(index) => PieChartSectionData(
|
|
value: entries
|
|
.where(
|
|
(element) => element.category.id == categories[index].id)
|
|
.fold<double>(
|
|
0,
|
|
(previousValue, element) =>
|
|
previousValue + element.data.amount,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|