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> DebitsAbs { get; set; } = new();
|
||||||
public List<decimal> Credits { get; set; } = new();
|
public List<decimal> Credits { get; set; } = new();
|
||||||
public List<decimal> Net { get; set; } = new();
|
public List<decimal> Net { get; set; } = new();
|
||||||
|
public List<decimal> RunningBalance { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
<div class="card shadow-sm h-100">
|
<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">
|
<div class="card-body">
|
||||||
<canvas id="trendChart" height="220"></canvas>
|
<canvas id="trendChart" height="220"></canvas>
|
||||||
</div>
|
</div>
|
||||||
@@ -180,9 +180,7 @@
|
|||||||
const topValues = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.TopCategories.Select(c => c.TotalSpend)));
|
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 trendLabels = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.TrendLabels));
|
||||||
const trendDebits = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.TrendDebitsAbs));
|
const trendBalance = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.TrendRunningBalance));
|
||||||
const trendCredits = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.TrendCredits));
|
|
||||||
const trendNet = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.TrendNet));
|
|
||||||
|
|
||||||
const catCtx = document.getElementById('categoryChart');
|
const catCtx = document.getElementById('categoryChart');
|
||||||
if (catCtx && topLabels.length) {
|
if (catCtx && topLabels.length) {
|
||||||
@@ -205,21 +203,46 @@
|
|||||||
const trendCtx = document.getElementById('trendChart');
|
const trendCtx = document.getElementById('trendChart');
|
||||||
if (trendCtx) {
|
if (trendCtx) {
|
||||||
new Chart(trendCtx, {
|
new Chart(trendCtx, {
|
||||||
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
labels: trendLabels,
|
labels: trendLabels,
|
||||||
datasets: [
|
datasets: [{
|
||||||
{ type: 'bar', label: 'Debits', data: trendDebits, backgroundColor: 'rgba(225,87,89,0.6)' },
|
label: 'Net Cash Flow',
|
||||||
{ type: 'bar', label: 'Credits', data: trendCredits, backgroundColor: 'rgba(76,175,80,0.6)' },
|
data: trendBalance,
|
||||||
{ type: 'line', label: 'Net', data: trendNet, borderColor: '#4e79a7', backgroundColor: 'transparent', tension: 0.2, yAxisID: 'y' }
|
borderColor: '#6ea8fe',
|
||||||
]
|
backgroundColor: 'rgba(110,168,254,0.15)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.3,
|
||||||
|
pointRadius: 0,
|
||||||
|
pointHoverRadius: 5
|
||||||
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
scales: {
|
scales: {
|
||||||
y: { beginAtZero: true, ticks: { color: '#adb5bd' }, grid: { color: 'rgba(255,255,255,0.1)' } },
|
y: {
|
||||||
x: { ticks: { color: '#adb5bd' }, grid: { color: 'rgba(255,255,255,0.1)' } }
|
ticks: {
|
||||||
|
color: '#adb5bd',
|
||||||
|
callback: function(value) { return '$' + value.toLocaleString(); }
|
||||||
},
|
},
|
||||||
plugins: { legend: { labels: { color: '#adb5bd' } } },
|
grid: { color: 'rgba(255,255,255,0.1)' }
|
||||||
maintainAspectRatio: false
|
},
|
||||||
|
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<TopCategoryRow> TopCategories { get; set; } = new();
|
||||||
public List<RecentTransactionRow> Recent { get; set; } = new();
|
public List<RecentTransactionRow> Recent { get; set; } = new();
|
||||||
public List<string> TrendLabels { get; set; } = new();
|
public List<string> TrendLabels { get; set; } = new();
|
||||||
public List<decimal> TrendDebitsAbs { get; set; } = new();
|
public List<decimal> TrendRunningBalance { get; set; } = new();
|
||||||
public List<decimal> TrendCredits { get; set; } = new();
|
|
||||||
public List<decimal> TrendNet { get; set; } = new();
|
|
||||||
|
|
||||||
public async Task OnGet()
|
public async Task OnGet()
|
||||||
{
|
{
|
||||||
@@ -29,9 +27,7 @@ namespace MoneyMap.Pages
|
|||||||
TopCategories = dashboard.TopCategories;
|
TopCategories = dashboard.TopCategories;
|
||||||
Recent = dashboard.RecentTransactions;
|
Recent = dashboard.RecentTransactions;
|
||||||
TrendLabels = dashboard.Trends.Labels;
|
TrendLabels = dashboard.Trends.Labels;
|
||||||
TrendDebitsAbs = dashboard.Trends.DebitsAbs;
|
TrendRunningBalance = dashboard.Trends.RunningBalance;
|
||||||
TrendCredits = dashboard.Trends.Credits;
|
|
||||||
TrendNet = dashboard.Trends.Net;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -231,10 +231,12 @@ namespace MoneyMap.Services
|
|||||||
var debitsAbs = new List<decimal>();
|
var debitsAbs = new List<decimal>();
|
||||||
var credits = new List<decimal>();
|
var credits = new List<decimal>();
|
||||||
var net = 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))
|
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))
|
if (dict.TryGetValue(d, out var v))
|
||||||
{
|
{
|
||||||
var debit = v.Debits;
|
var debit = v.Debits;
|
||||||
@@ -242,6 +244,7 @@ namespace MoneyMap.Services
|
|||||||
debitsAbs.Add(Math.Abs(debit));
|
debitsAbs.Add(Math.Abs(debit));
|
||||||
credits.Add(credit);
|
credits.Add(credit);
|
||||||
net.Add(credit + debit);
|
net.Add(credit + debit);
|
||||||
|
cumulative += credit + debit;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -249,6 +252,7 @@ namespace MoneyMap.Services
|
|||||||
credits.Add(0);
|
credits.Add(0);
|
||||||
net.Add(0);
|
net.Add(0);
|
||||||
}
|
}
|
||||||
|
runningBalance.Add(cumulative);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SpendTrends
|
return new SpendTrends
|
||||||
@@ -256,7 +260,8 @@ namespace MoneyMap.Services
|
|||||||
Labels = labels,
|
Labels = labels,
|
||||||
DebitsAbs = debitsAbs,
|
DebitsAbs = debitsAbs,
|
||||||
Credits = credits,
|
Credits = credits,
|
||||||
Net = net
|
Net = net,
|
||||||
|
RunningBalance = runningBalance
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user