From 23480d80d27fff786782e5b8e9df0381bf7be4f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Wed, 22 Nov 2023 15:09:05 +0100 Subject: [PATCH] feat(graphs): :sparkles: render income graph next to expense graph #8 --- .vscode/settings.json | 3 +- lib/l10n/app_cs.arb | 4 +-- lib/l10n/app_en.arb | 4 +-- lib/util/graphs.dart | 67 +++++++++++++++++++++++++++++---------- lib/views/graph_view.dart | 47 +++++++++++++++++++-------- lib/views/home.dart | 6 ++-- 6 files changed, 92 insertions(+), 39 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 4397789..9aa5bb4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "conventionalCommits.scopes": [ "ocr", "ui", - "translations" + "translations", + "graphs" ] } \ No newline at end of file diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb index 3f32913..58a74db 100644 --- a/lib/l10n/app_cs.arb +++ b/lib/l10n/app_cs.arb @@ -59,8 +59,8 @@ "setupStartingBalance":"Počáteční zůstatek", "graphs":"Grafy", "createTestData":"Vytvořit vzorková data", - "spendingStats":"Statistiky utrácení", "yearly":"Roční", - "monthly":"Měsíční" + "monthly":"Měsíční", + "expenses":"Výdaje" } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3fd1003..1b49332 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -95,7 +95,7 @@ "setupStartingBalance":"Starting balance", "graphs":"Graphs", "createTestData":"Create test data", - "spendingStats":"Spending statistics", "yearly":"Yearly", - "monthly":"Monthly" + "monthly":"Monthly", + "expenses":"Expenses" } \ No newline at end of file diff --git a/lib/util/graphs.dart b/lib/util/graphs.dart index c91fa06..5227e20 100644 --- a/lib/util/graphs.dart +++ b/lib/util/graphs.dart @@ -1,47 +1,80 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:prasule/main.dart'; -/// Monthly/Yearly expenses [LineChart] +/// Monthly/Yearly expense/income [LineChart] class ExpensesChart extends StatelessWidget { const ExpensesChart( {super.key, required this.date, required this.locale, - this.data = const [], + this.expenseData = const [], + this.incomeData = const [], this.yearly = false}); final bool yearly; final DateTime date; final String locale; - final List data; - List get dataSorted { - var list = List.from(data); + final List expenseData; + 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( maxX: (yearly) ? 12 : DateTime(date.year, date.month, 0).day.toDouble(), - maxY: dataSorted.last, + maxY: maxY, minX: 1, minY: 0, backgroundColor: Theme.of(context).colorScheme.background, lineBarsData: [ - 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() + 1, data[index]), + 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() + 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 titlesData: FlTitlesData( rightTitles: const AxisTitles( diff --git a/lib/views/graph_view.dart b/lib/views/graph_view.dart index e03c7c6..ed69922 100644 --- a/lib/views/graph_view.dart +++ b/lib/views/graph_view.dart @@ -1,6 +1,7 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:prasule/api/category.dart'; import 'package:prasule/api/wallet.dart'; import 'package:prasule/api/walletmanager.dart'; import 'package:prasule/main.dart'; @@ -25,6 +26,7 @@ class _GraphViewState extends State { List wallets = []; String? locale; var yearlyBtnSet = {"monthly"}; + var graphTypeSet = {"expense", "income"}; bool get yearly => yearlyBtnSet.contains("yearly"); @override @@ -33,20 +35,22 @@ class _GraphViewState extends State { locale ??= Localizations.localeOf(context).languageCode; } - List generateChartData() { - if (selectedWallet == null) return [0]; + List generateChartData(EntryType type) { var data = List.filled( (yearly) ? 12 : DateTime(_selectedDate.year, _selectedDate.month, 0).day, 0.0); + if (selectedWallet == null) return []; for (var i = 0; i < data.length; i++) { - var entriesForRange = selectedWallet!.entries.where((element) => (!yearly) - ? element.date.month == _selectedDate.month && - element.date.year == _selectedDate.year && - element.date.day == i + 1 - : element.date.month == i + 1 && - element.date.year == _selectedDate.year); + var entriesForRange = selectedWallet!.entries.where((element) => + ((!yearly) + ? element.date.month == _selectedDate.month && + element.date.year == _selectedDate.year && + element.date.day == i + 1 + : element.date.month == i + 1 && + element.date.year == _selectedDate.year) && + element.type == type); var sum = 0.0; for (var e in entriesForRange) { sum += e.data.amount; @@ -148,12 +152,20 @@ class _GraphViewState extends State { SegmentedButton( segments: [ ButtonSegment( - value: "spending", - label: Text(AppLocalizations.of(context)!.spendingStats), - ) + value: "expense", + label: Text(AppLocalizations.of(context)!.expenses), + ), + ButtonSegment( + value: "income", + label: Text(AppLocalizations.of(context)!.income), + ), ], - selected: const {"spending"}, - // TODO: onSelectionChanged + selected: graphTypeSet, + multiSelectionEnabled: true, + onSelectionChanged: (selection) { + graphTypeSet = selection; + setState(() {}); + }, ), const SizedBox( height: 5, @@ -189,6 +201,8 @@ class _GraphViewState extends State { ..sort((a, b) => b.date.compareTo(a.date))) .first .date; + logger.i(firstDate); + logger.i(lastDate); var newDate = await showDatePicker( context: context, initialDate: _selectedDate, @@ -215,7 +229,12 @@ class _GraphViewState extends State { date: _selectedDate, locale: locale ?? "en", yearly: yearly, - data: generateChartData(), + expenseData: (graphTypeSet.contains("expense")) + ? generateChartData(EntryType.expense) + : [], + incomeData: (graphTypeSet.contains("income")) + ? generateChartData(EntryType.income) + : [], ), ) ], diff --git a/lib/views/home.dart b/lib/views/home.dart index 6d6e5ef..cf38e6f 100644 --- a/lib/views/home.dart +++ b/lib/views/home.dart @@ -1,6 +1,4 @@ import 'dart:math'; - -import 'package:currency_picker/currency_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; @@ -87,7 +85,9 @@ class _HomeViewState extends State { name: "Test Entry #${i + 1}", amount: random.nextInt(20000).toDouble(), ), - type: EntryType.expense, + type: (random.nextInt(3) > 0) + ? EntryType.expense + : EntryType.income, date: DateTime( 2023, random.nextInt(12) + 1,