feat: implement aggregated and per-account balance visualization in statistics page with interactive chart options

This commit is contained in:
Julien Froidefond
2025-11-28 14:01:26 +01:00
parent fc483d0573
commit b2db902e5c
2 changed files with 189 additions and 26 deletions

View File

@@ -119,27 +119,84 @@ export default function StatisticsPage() {
const avgMonthlyExpenses =
monthlyData.size > 0 ? totalExpenses / monthlyData.size : 0;
// Balance evolution
const sortedTransactions = [...transactions].sort(
// Balance evolution - Aggregated
const allTransactionsForBalance = data.transactions.filter(
(t) => new Date(t.date) >= startDate
);
const sortedAllTransactions = [...allTransactionsForBalance].sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
);
let runningBalance = 0;
const balanceByDate = new Map<string, number>();
sortedTransactions.forEach((t) => {
const aggregatedBalanceByDate = new Map<string, number>();
sortedAllTransactions.forEach((t) => {
runningBalance += t.amount;
balanceByDate.set(t.date, runningBalance);
aggregatedBalanceByDate.set(t.date, runningBalance);
});
const balanceChartData = Array.from(balanceByDate.entries()).map(
([date, balance]) => ({
const aggregatedBalanceData = Array.from(
aggregatedBalanceByDate.entries()
).map(([date, balance]) => ({
date: new Date(date).toLocaleDateString("fr-FR", {
day: "2-digit",
month: "short",
}),
solde: Math.round(balance),
}));
// Balance evolution - Per account
const accountBalances = new Map<string, Map<string, number>>();
data.accounts.forEach((account) => {
accountBalances.set(account.id, new Map());
});
// Calculate running balance per account
const accountRunningBalances = new Map<string, number>();
data.accounts.forEach((account) => {
accountRunningBalances.set(account.id, 0);
});
sortedAllTransactions.forEach((t) => {
const currentBalance = accountRunningBalances.get(t.accountId) || 0;
const newBalance = currentBalance + t.amount;
accountRunningBalances.set(t.accountId, newBalance);
const accountDates = accountBalances.get(t.accountId);
if (accountDates) {
accountDates.set(t.date, newBalance);
}
});
// Merge all dates and create data points
const allDates = new Set<string>();
accountBalances.forEach((dates) => {
dates.forEach((_, date) => allDates.add(date));
});
const sortedDates = Array.from(allDates).sort();
const lastBalances = new Map<string, number>();
data.accounts.forEach((account) => {
lastBalances.set(account.id, 0);
});
const perAccountBalanceData = sortedDates.map((date) => {
const point: { date: string; [key: string]: string | number } = {
date: new Date(date).toLocaleDateString("fr-FR", {
day: "2-digit",
month: "short",
}),
solde: Math.round(balance),
})
);
};
data.accounts.forEach((account) => {
const accountDates = accountBalances.get(account.id);
if (accountDates?.has(date)) {
lastBalances.set(account.id, accountDates.get(date)!);
}
point[account.id] = Math.round(lastBalances.get(account.id) || 0);
});
return point;
});
return {
monthlyChartData,
@@ -148,7 +205,8 @@ export default function StatisticsPage() {
totalIncome,
totalExpenses,
avgMonthlyExpenses,
balanceChartData,
aggregatedBalanceData,
perAccountBalanceData,
transactionCount: transactions.length,
};
}, [data, period, selectedAccount]);
@@ -219,7 +277,9 @@ export default function StatisticsPage() {
formatCurrency={formatCurrency}
/>
<BalanceLineChart
data={stats.balanceChartData}
aggregatedData={stats.aggregatedBalanceData}
perAccountData={stats.perAccountBalanceData}
accounts={data.accounts}
formatCurrency={formatCurrency}
/>
<TopExpensesList