feat: add graphs #16
6 changed files with 92 additions and 39 deletions
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -2,6 +2,7 @@
|
||||||
"conventionalCommits.scopes": [
|
"conventionalCommits.scopes": [
|
||||||
"ocr",
|
"ocr",
|
||||||
"ui",
|
"ui",
|
||||||
"translations"
|
"translations",
|
||||||
|
"graphs"
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -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"
|
||||||
|
|
||||||
}
|
}
|
|
@ -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"
|
||||||
}
|
}
|
|
@ -1,35 +1,55 @@
|
||||||
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: [
|
||||||
|
if (incomeData.isNotEmpty)
|
||||||
LineChartBarData(
|
LineChartBarData(
|
||||||
isCurved: true,
|
isCurved: true,
|
||||||
barWidth: 8,
|
barWidth: 8,
|
||||||
|
@ -39,7 +59,20 @@ class ExpensesChart extends StatelessWidget {
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
spots: List.generate(
|
spots: List.generate(
|
||||||
(yearly) ? 12 : DateTime(date.year, date.month, 0).day,
|
(yearly) ? 12 : DateTime(date.year, date.month, 0).day,
|
||||||
(index) => FlSpot(index.toDouble() + 1, data[index]),
|
(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
|
||||||
|
|
|
@ -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) =>
|
||||||
|
((!yearly)
|
||||||
? element.date.month == _selectedDate.month &&
|
? element.date.month == _selectedDate.month &&
|
||||||
element.date.year == _selectedDate.year &&
|
element.date.year == _selectedDate.year &&
|
||||||
element.date.day == i + 1
|
element.date.day == i + 1
|
||||||
: element.date.month == i + 1 &&
|
: element.date.month == i + 1 &&
|
||||||
element.date.year == _selectedDate.year);
|
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)
|
||||||
|
: [],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue