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:
@@ -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>
|
||||
|
||||
@@ -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(); }
|
||||
},
|
||||
plugins: { legend: { labels: { color: '#adb5bd' } } },
|
||||
maintainAspectRatio: false
|
||||
grid: { color: 'rgba(255,255,255,0.1)' }
|
||||
},
|
||||
x: {
|
||||
ticks: { color: '#adb5bd', maxTicksLimit: 10 },
|
||||
grid: { color: 'rgba(255,255,255,0.05)' }
|
||||
}
|
||||
},
|
||||
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' }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user