prasule/lib/util/graphs.dart
2023-12-29 19:43:27 +01:00

326 lines
12 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: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<double> expenseData;
final Currency currency;
List<double> get expenseDataSorted {
var list = List<double>.from(expenseData);
list.sort((a, b) => a.compareTo(b));
return list;
}
final List<double> incomeData;
List<double> get incomeDataSorted {
var list = List<double>.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<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, 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<double> expenseData;
List<double> get expenseDataSorted {
var list = List<double>.from(expenseData);
list.sort((a, b) => a.compareTo(b));
return list;
}
final Currency currency;
final List<double> incomeData;
List<double> get incomeDataSorted {
var list = List<double>.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<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,
),
),
),
),
);
}