feat(graphs): render income graph next to expense graph

#8
This commit is contained in:
Matyáš Caras 2023-11-22 15:09:05 +01:00
parent d889611e19
commit 23480d80d2
Signed by untrusted user who does not match committer: hernik
GPG key ID: 2A3175F98820C5C6
6 changed files with 92 additions and 39 deletions

View file

@ -2,6 +2,7 @@
"conventionalCommits.scopes": [ "conventionalCommits.scopes": [
"ocr", "ocr",
"ui", "ui",
"translations" "translations",
"graphs"
] ]
} }

View file

@ -59,8 +59,8 @@
"setupStartingBalance":"Počáteční zůstatek", "setupStartingBalance":"Počáteční zůstatek",
"graphs":"Grafy", "graphs":"Grafy",
"createTestData":"Vytvořit vzorková data", "createTestData":"Vytvořit vzorková data",
"spendingStats":"Statistiky utrácení",
"yearly":"Roční", "yearly":"Roční",
"monthly":"Měsíční" "monthly":"Měsíční",
"expenses":"Výdaje"
} }

View file

@ -95,7 +95,7 @@
"setupStartingBalance":"Starting balance", "setupStartingBalance":"Starting balance",
"graphs":"Graphs", "graphs":"Graphs",
"createTestData":"Create test data", "createTestData":"Create test data",
"spendingStats":"Spending statistics",
"yearly":"Yearly", "yearly":"Yearly",
"monthly":"Monthly" "monthly":"Monthly",
"expenses":"Expenses"
} }

View file

@ -1,47 +1,80 @@
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:prasule/main.dart';
/// Monthly/Yearly expenses [LineChart] /// Monthly/Yearly expense/income [LineChart]
class ExpensesChart extends StatelessWidget { class ExpensesChart extends StatelessWidget {
const ExpensesChart( const ExpensesChart(
{super.key, {super.key,
required this.date, required this.date,
required this.locale, required this.locale,
this.data = const [], this.expenseData = const [],
this.incomeData = const [],
this.yearly = false}); this.yearly = false});
final bool yearly; final bool yearly;
final DateTime date; final DateTime date;
final String locale; final String locale;
final List<double> data; final List<double> expenseData;
List<double> get dataSorted { List<double> get expenseDataSorted {
var list = List<double>.from(data); var list = List<double>.from(expenseData);
list.sort((a, b) => a.compareTo(b)); list.sort((a, b) => a.compareTo(b));
return list; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LineChart( return LineChart(
LineChartData( LineChartData(
maxX: (yearly) ? 12 : DateTime(date.year, date.month, 0).day.toDouble(), maxX: (yearly) ? 12 : DateTime(date.year, date.month, 0).day.toDouble(),
maxY: dataSorted.last, maxY: maxY,
minX: 1, minX: 1,
minY: 0, minY: 0,
backgroundColor: Theme.of(context).colorScheme.background, backgroundColor: Theme.of(context).colorScheme.background,
lineBarsData: [ lineBarsData: [
LineChartBarData( if (incomeData.isNotEmpty)
isCurved: true, LineChartBarData(
barWidth: 8, isCurved: true,
isStrokeCapRound: true, barWidth: 8,
dotData: const FlDotData(show: false), isStrokeCapRound: true,
belowBarData: BarAreaData(show: false), dotData: const FlDotData(show: false),
color: Theme.of(context).colorScheme.primary, belowBarData: BarAreaData(show: false),
spots: List.generate( color: Theme.of(context).colorScheme.primary,
(yearly) ? 12 : DateTime(date.year, date.month, 0).day, spots: List.generate(
(index) => FlSpot(index.toDouble() + 1, data[index]), (yearly) ? 12 : DateTime(date.year, date.month, 0).day,
(index) => FlSpot(index.toDouble() + 1, 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 ], // actual data
titlesData: FlTitlesData( titlesData: FlTitlesData(
rightTitles: const AxisTitles( rightTitles: const AxisTitles(

View file

@ -1,6 +1,7 @@
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:prasule/api/category.dart';
import 'package:prasule/api/wallet.dart'; import 'package:prasule/api/wallet.dart';
import 'package:prasule/api/walletmanager.dart'; import 'package:prasule/api/walletmanager.dart';
import 'package:prasule/main.dart'; import 'package:prasule/main.dart';
@ -25,6 +26,7 @@ class _GraphViewState extends State<GraphView> {
List<Wallet> wallets = []; List<Wallet> wallets = [];
String? locale; String? locale;
var yearlyBtnSet = {"monthly"}; var yearlyBtnSet = {"monthly"};
var graphTypeSet = {"expense", "income"};
bool get yearly => yearlyBtnSet.contains("yearly"); bool get yearly => yearlyBtnSet.contains("yearly");
@override @override
@ -33,20 +35,22 @@ class _GraphViewState extends State<GraphView> {
locale ??= Localizations.localeOf(context).languageCode; locale ??= Localizations.localeOf(context).languageCode;
} }
List<double> generateChartData() { List<double> generateChartData(EntryType type) {
if (selectedWallet == null) return [0];
var data = List<double>.filled( var data = List<double>.filled(
(yearly) (yearly)
? 12 ? 12
: DateTime(_selectedDate.year, _selectedDate.month, 0).day, : DateTime(_selectedDate.year, _selectedDate.month, 0).day,
0.0); 0.0);
if (selectedWallet == null) return [];
for (var i = 0; i < data.length; i++) { for (var i = 0; i < data.length; i++) {
var entriesForRange = selectedWallet!.entries.where((element) => (!yearly) var entriesForRange = selectedWallet!.entries.where((element) =>
? element.date.month == _selectedDate.month && ((!yearly)
element.date.year == _selectedDate.year && ? element.date.month == _selectedDate.month &&
element.date.day == i + 1 element.date.year == _selectedDate.year &&
: element.date.month == i + 1 && element.date.day == i + 1
element.date.year == _selectedDate.year); : element.date.month == i + 1 &&
element.date.year == _selectedDate.year) &&
element.type == type);
var sum = 0.0; var sum = 0.0;
for (var e in entriesForRange) { for (var e in entriesForRange) {
sum += e.data.amount; sum += e.data.amount;
@ -148,12 +152,20 @@ class _GraphViewState extends State<GraphView> {
SegmentedButton<String>( SegmentedButton<String>(
segments: [ segments: [
ButtonSegment<String>( ButtonSegment<String>(
value: "spending", value: "expense",
label: Text(AppLocalizations.of(context)!.spendingStats), label: Text(AppLocalizations.of(context)!.expenses),
) ),
ButtonSegment<String>(
value: "income",
label: Text(AppLocalizations.of(context)!.income),
),
], ],
selected: const {"spending"}, selected: graphTypeSet,
// TODO: onSelectionChanged multiSelectionEnabled: true,
onSelectionChanged: (selection) {
graphTypeSet = selection;
setState(() {});
},
), ),
const SizedBox( const SizedBox(
height: 5, height: 5,
@ -189,6 +201,8 @@ class _GraphViewState extends State<GraphView> {
..sort((a, b) => b.date.compareTo(a.date))) ..sort((a, b) => b.date.compareTo(a.date)))
.first .first
.date; .date;
logger.i(firstDate);
logger.i(lastDate);
var newDate = await showDatePicker( var newDate = await showDatePicker(
context: context, context: context,
initialDate: _selectedDate, initialDate: _selectedDate,
@ -215,7 +229,12 @@ class _GraphViewState extends State<GraphView> {
date: _selectedDate, date: _selectedDate,
locale: locale ?? "en", locale: locale ?? "en",
yearly: yearly, yearly: yearly,
data: generateChartData(), expenseData: (graphTypeSet.contains("expense"))
? generateChartData(EntryType.expense)
: [],
incomeData: (graphTypeSet.contains("income"))
? generateChartData(EntryType.income)
: [],
), ),
) )
], ],

View file

@ -1,6 +1,4 @@
import 'dart:math'; import 'dart:math';
import 'package:currency_picker/currency_picker.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_slidable/flutter_slidable.dart';
@ -87,7 +85,9 @@ class _HomeViewState extends State<HomeView> {
name: "Test Entry #${i + 1}", name: "Test Entry #${i + 1}",
amount: random.nextInt(20000).toDouble(), amount: random.nextInt(20000).toDouble(),
), ),
type: EntryType.expense, type: (random.nextInt(3) > 0)
? EntryType.expense
: EntryType.income,
date: DateTime( date: DateTime(
2023, 2023,
random.nextInt(12) + 1, random.nextInt(12) + 1,