UI: Replace cash flow bar chart with running balance line

- Add RunningBalance to SpendTrends model
- Calculate cumulative balance over 30 days
- Display as smooth filled line chart (neutral blue)
- Cleaner date labels (MMM d format)
- Better tooltip formatting with $ values

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-05 22:05:09 -05:00
parent 55e758a42a
commit f5cfd982cd
4 changed files with 46 additions and 21 deletions

View File

@@ -47,6 +47,7 @@ namespace MoneyMap.Models.Dashboard
public List<decimal> DebitsAbs { get; set; } = new();
public List<decimal> Credits { get; set; } = new();
public List<decimal> Net { get; set; } = new();
public List<decimal> RunningBalance { get; set; } = new();
}
/// <summary>

View File

@@ -68,7 +68,7 @@
</div>
<div class="col-lg-6">
<div class="card shadow-sm h-100">
<div class="card-header">Cash flow (last 30 days)</div>
<div class="card-header">Net cash flow (last 30 days)</div>
<div class="card-body">
<canvas id="trendChart" height="220"></canvas>
</div>
@@ -180,9 +180,7 @@
const topValues = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.TopCategories.Select(c => c.TotalSpend)));
const trendLabels = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.TrendLabels));
const trendDebits = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.TrendDebitsAbs));
const trendCredits = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.TrendCredits));
const trendNet = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.TrendNet));
const trendBalance = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.TrendRunningBalance));
const catCtx = document.getElementById('categoryChart');
if (catCtx && topLabels.length) {
@@ -205,21 +203,46 @@
const trendCtx = document.getElementById('trendChart');
if (trendCtx) {
new Chart(trendCtx, {
type: 'line',
data: {
labels: trendLabels,
datasets: [
{ type: 'bar', label: 'Debits', data: trendDebits, backgroundColor: 'rgba(225,87,89,0.6)' },
{ type: 'bar', label: 'Credits', data: trendCredits, backgroundColor: 'rgba(76,175,80,0.6)' },
{ type: 'line', label: 'Net', data: trendNet, borderColor: '#4e79a7', backgroundColor: 'transparent', tension: 0.2, yAxisID: 'y' }
]
datasets: [{
label: 'Net Cash Flow',
data: trendBalance,
borderColor: '#6ea8fe',
backgroundColor: 'rgba(110,168,254,0.15)',
fill: true,
tension: 0.3,
pointRadius: 0,
pointHoverRadius: 5
}]
},
options: {
scales: {
y: { beginAtZero: true, ticks: { color: '#adb5bd' }, grid: { color: 'rgba(255,255,255,0.1)' } },
x: { ticks: { color: '#adb5bd' }, grid: { color: 'rgba(255,255,255,0.1)' } }
y: {
ticks: {
color: '#adb5bd',
callback: function(value) { return '$' + value.toLocaleString(); }
},
grid: { color: 'rgba(255,255,255,0.1)' }
},
x: {
ticks: { color: '#adb5bd', maxTicksLimit: 10 },
grid: { color: 'rgba(255,255,255,0.05)' }
}
},
plugins: { legend: { labels: { color: '#adb5bd' } } },
maintainAspectRatio: false
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: function(context) {
return '$' + context.parsed.y.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2});
}
}
}
},
maintainAspectRatio: false,
interaction: { intersect: false, mode: 'index' }
}
});
}

View File

@@ -17,9 +17,7 @@ namespace MoneyMap.Pages
public List<TopCategoryRow> TopCategories { get; set; } = new();
public List<RecentTransactionRow> Recent { get; set; } = new();
public List<string> TrendLabels { get; set; } = new();
public List<decimal> TrendDebitsAbs { get; set; } = new();
public List<decimal> TrendCredits { get; set; } = new();
public List<decimal> TrendNet { get; set; } = new();
public List<decimal> TrendRunningBalance { get; set; } = new();
public async Task OnGet()
{
@@ -29,9 +27,7 @@ namespace MoneyMap.Pages
TopCategories = dashboard.TopCategories;
Recent = dashboard.RecentTransactions;
TrendLabels = dashboard.Trends.Labels;
TrendDebitsAbs = dashboard.Trends.DebitsAbs;
TrendCredits = dashboard.Trends.Credits;
TrendNet = dashboard.Trends.Net;
TrendRunningBalance = dashboard.Trends.RunningBalance;
}
}
}

View File

@@ -231,10 +231,12 @@ namespace MoneyMap.Services
var debitsAbs = new List<decimal>();
var credits = new List<decimal>();
var net = new List<decimal>();
var runningBalance = new List<decimal>();
decimal cumulative = 0;
for (var d = since; d <= today; d = d.AddDays(1))
{
labels.Add(d.ToString("yyyy-MM-dd"));
labels.Add(d.ToString("MMM d"));
if (dict.TryGetValue(d, out var v))
{
var debit = v.Debits;
@@ -242,6 +244,7 @@ namespace MoneyMap.Services
debitsAbs.Add(Math.Abs(debit));
credits.Add(credit);
net.Add(credit + debit);
cumulative += credit + debit;
}
else
{
@@ -249,6 +252,7 @@ namespace MoneyMap.Services
credits.Add(0);
net.Add(0);
}
runningBalance.Add(cumulative);
}
return new SpendTrends
@@ -256,7 +260,8 @@ namespace MoneyMap.Services
Labels = labels,
DebitsAbs = debitsAbs,
Credits = credits,
Net = net
Net = net,
RunningBalance = runningBalance
};
}
}