Banking — Spending Breakdown
A trust-first spending breakdown widget for banking and fintech dashboards. An SVG donut chart splits the month's spend by category, with the running total centred inside the ring and a colour-matched legend listing every category by amount and share. Hovering or clicking a slice highlights its legend row and surfaces that category's amount in the centre, while a month switcher steps through four months of real-feeling data and recomputes totals, the budget pill, and a month-over-month delta. Tabular figures, keyboard support, and vanilla JS only.
MCP
Code
:root {
--navy: #0e1b3a;
--navy-2: #16264d;
--ink: #0e1726;
--ink-2: #3a4660;
--muted: #697089;
--accent: #3b6ef6;
--accent-d: #2a55cc;
--accent-50: #eaf0ff;
--teal: #0fb5a6;
--violet: #7c5cff;
--bg: #f5f7fb;
--surface: #ffffff;
--line: rgba(14, 27, 58, 0.1);
--line-2: rgba(14, 27, 58, 0.18);
--ok: #1f9d62;
--warn: #d9982b;
--danger: #d4493e;
--credit: #1f9d62;
--debit: #0e1726;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-1: 0 1px 2px rgba(14, 27, 58, 0.06), 0 1px 3px rgba(14, 27, 58, 0.05);
--sh-2: 0 8px 24px rgba(14, 27, 58, 0.1), 0 2px 6px rgba(14, 27, 58, 0.06);
--sh-3: 0 18px 50px rgba(14, 27, 58, 0.14);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
padding: 28px 16px;
background:
radial-gradient(1200px 540px at 12% -10%, rgba(59, 110, 246, 0.1), transparent 60%),
radial-gradient(900px 480px at 110% 0%, rgba(124, 92, 255, 0.08), transparent 55%),
var(--bg);
color: var(--ink);
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-variant-numeric: tabular-nums;
}
.wrap {
width: 100%;
max-width: 720px;
}
/* ---------- Card shell ---------- */
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-3);
overflow: hidden;
}
.card__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 20px 22px 16px;
background: linear-gradient(180deg, #fbfcff, #ffffff);
border-bottom: 1px solid var(--line);
}
.head__lead {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.brand {
display: grid;
place-items: center;
width: 38px;
height: 38px;
flex: none;
border-radius: 12px;
color: #fff;
background: linear-gradient(135deg, var(--navy), var(--navy-2));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.16);
}
h1 {
margin: 0;
font-size: 1.05rem;
font-weight: 700;
letter-spacing: -0.01em;
color: var(--ink);
}
.sub {
margin: 1px 0 0;
font-size: 0.8rem;
color: var(--muted);
font-weight: 500;
}
.masked {
letter-spacing: 0.06em;
}
/* ---------- Month switcher ---------- */
.month-switch {
display: inline-flex;
align-items: center;
gap: 2px;
flex: none;
padding: 4px;
border: 1px solid var(--line);
border-radius: 999px;
background: var(--bg);
box-shadow: var(--sh-1);
}
.month-btn {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border: none;
border-radius: 999px;
background: var(--surface);
color: var(--ink-2);
cursor: pointer;
transition: background 0.15s, color 0.15s, transform 0.08s;
}
.month-btn:hover {
background: var(--accent-50);
color: var(--accent-d);
}
.month-btn:active {
transform: scale(0.92);
}
.month-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.month-label {
min-width: 108px;
text-align: center;
font-size: 0.82rem;
font-weight: 600;
color: var(--ink);
}
/* ---------- Body layout ---------- */
.body {
display: grid;
grid-template-columns: 232px 1fr;
gap: 26px;
padding: 24px 22px;
align-items: center;
}
/* ---------- Donut ---------- */
.chart {
margin: 0;
display: grid;
place-items: center;
}
.donut {
position: relative;
width: 212px;
height: 212px;
}
.donut__svg {
width: 100%;
height: 100%;
display: block;
}
.donut__svg .seg {
fill: none;
stroke-width: 26;
stroke-linecap: butt;
cursor: pointer;
transition: stroke-width 0.2s ease, opacity 0.2s ease;
transform-box: fill-box;
}
.donut.has-active .seg {
opacity: 0.32;
}
.donut .seg.is-active {
opacity: 1;
stroke-width: 31;
}
.donut__center {
position: absolute;
inset: 0;
display: grid;
place-content: center;
text-align: center;
pointer-events: none;
gap: 2px;
}
.center__label {
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--muted);
}
.center__total {
font-size: 1.72rem;
font-weight: 800;
letter-spacing: -0.02em;
color: var(--ink);
font-variant-numeric: tabular-nums;
}
.center__delta {
font-size: 0.75rem;
font-weight: 600;
color: var(--muted);
}
.center__delta.up {
color: var(--danger);
}
.center__delta.down {
color: var(--ok);
}
/* ---------- Legend ---------- */
.legend {
min-width: 0;
}
.legend__head {
display: grid;
grid-template-columns: 1fr auto 56px;
gap: 10px;
padding: 0 8px 8px;
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--muted);
border-bottom: 1px solid var(--line);
}
.ta-r {
text-align: right;
}
.legend__list {
list-style: none;
margin: 0;
padding: 4px 0;
display: flex;
flex-direction: column;
}
.leg {
display: grid;
grid-template-columns: 1fr auto 56px;
align-items: center;
gap: 10px;
width: 100%;
padding: 9px 8px;
border: none;
border-radius: var(--r-sm);
background: transparent;
text-align: left;
font: inherit;
color: inherit;
cursor: pointer;
transition: background 0.15s;
}
.leg:hover,
.leg:focus-visible {
background: var(--bg);
outline: none;
}
.leg.is-active {
background: var(--accent-50);
}
.leg__name {
display: flex;
align-items: center;
gap: 9px;
min-width: 0;
font-size: 0.86rem;
font-weight: 600;
color: var(--ink);
}
.dot {
width: 11px;
height: 11px;
flex: none;
border-radius: 4px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.leg__name span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.leg__amt {
text-align: right;
font-size: 0.86rem;
font-weight: 700;
color: var(--debit);
font-variant-numeric: tabular-nums;
}
.leg__pct {
text-align: right;
font-size: 0.8rem;
font-weight: 600;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
/* ---------- Legend footer ---------- */
.legend__foot {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-top: 10px;
padding-top: 14px;
border-top: 1px solid var(--line);
}
.foot__cell {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
}
.foot__k {
font-size: 0.66rem;
font-weight: 600;
letter-spacing: 0.03em;
text-transform: uppercase;
color: var(--muted);
}
.foot__v {
font-size: 0.92rem;
font-weight: 700;
color: var(--ink);
font-variant-numeric: tabular-nums;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pill {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 2px 9px;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.01em;
}
.pill--ok {
color: var(--ok);
background: rgba(31, 157, 98, 0.12);
}
.pill--warn {
color: var(--warn);
background: rgba(217, 152, 43, 0.14);
}
.pill--danger {
color: var(--danger);
background: rgba(212, 73, 62, 0.12);
}
/* ---------- Card footer ---------- */
.card__foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 13px 22px;
border-top: 1px solid var(--line);
background: linear-gradient(180deg, #ffffff, #fbfcff);
}
.secure {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.76rem;
font-weight: 600;
color: var(--muted);
}
.secure svg {
color: var(--ok);
}
.link-btn {
border: 1px solid var(--line-2);
background: var(--surface);
color: var(--accent-d);
font: inherit;
font-size: 0.8rem;
font-weight: 700;
padding: 7px 14px;
border-radius: 999px;
cursor: pointer;
transition: background 0.15s, border-color 0.15s, transform 0.08s;
}
.link-btn:hover {
background: var(--accent-50);
border-color: var(--accent);
}
.link-btn:active {
transform: translateY(1px);
}
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 16px);
background: var(--navy);
color: #fff;
font-size: 0.84rem;
font-weight: 600;
padding: 11px 18px;
border-radius: 999px;
box-shadow: var(--sh-3);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s, transform 0.22s;
z-index: 30;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
body {
padding: 16px 12px;
}
.card__head {
flex-wrap: wrap;
padding: 16px 16px 14px;
}
.month-switch {
width: 100%;
justify-content: space-between;
}
.month-label {
flex: 1;
}
.body {
grid-template-columns: 1fr;
gap: 20px;
padding: 20px 16px;
}
.donut {
width: 196px;
height: 196px;
}
.center__total {
font-size: 1.55rem;
}
.legend__foot {
grid-template-columns: 1fr 1fr;
}
.foot__cell:last-child {
grid-column: 1 / -1;
}
.card__foot {
padding: 12px 16px;
}
}(function () {
"use strict";
/* ------------------------------------------------------------------ *
* Fictional data — 4 months of categorised spend for a demo account. *
* ------------------------------------------------------------------ */
var PALETTE = {
Housing: "#3b6ef6",
Groceries: "#0fb5a6",
Dining: "#7c5cff",
Transport: "#d9982b",
Shopping: "#d4493e",
Subscriptions: "#16264d",
Health: "#1f9d62",
};
var MONTHS = [
{
label: "January 2026",
budget: 4200,
txns: 71,
topMerchant: "Harbor Realty",
cats: [
{ name: "Housing", amount: 1650.00 },
{ name: "Groceries", amount: 612.40 },
{ name: "Dining", amount: 388.15 },
{ name: "Transport", amount: 244.80 },
{ name: "Shopping", amount: 415.00 },
{ name: "Subscriptions", amount: 96.94 },
{ name: "Health", amount: 132.50 },
],
},
{
label: "February 2026",
budget: 4200,
txns: 64,
topMerchant: "Harbor Realty",
cats: [
{ name: "Housing", amount: 1650.00 },
{ name: "Groceries", amount: 548.20 },
{ name: "Dining", amount: 502.70 },
{ name: "Transport", amount: 198.35 },
{ name: "Shopping", amount: 286.40 },
{ name: "Subscriptions", amount: 96.94 },
{ name: "Health", amount: 64.00 },
],
},
{
label: "March 2026",
budget: 4200,
txns: 83,
topMerchant: "Lumen Market",
cats: [
{ name: "Housing", amount: 1650.00 },
{ name: "Groceries", amount: 701.85 },
{ name: "Dining", amount: 441.10 },
{ name: "Transport", amount: 312.60 },
{ name: "Shopping", amount: 689.25 },
{ name: "Subscriptions", amount: 114.93 },
{ name: "Health", amount: 208.40 },
],
},
{
label: "April 2026",
budget: 4200,
txns: 76,
topMerchant: "Aera Coffee",
cats: [
{ name: "Housing", amount: 1650.00 },
{ name: "Groceries", amount: 634.55 },
{ name: "Dining", amount: 528.90 },
{ name: "Transport", amount: 271.20 },
{ name: "Shopping", amount: 372.80 },
{ name: "Subscriptions", amount: 114.93 },
{ name: "Health", amount: 88.00 },
],
},
];
/* ------------------------------------------------------------------ */
var current = MONTHS.length - 1; // start on April 2026
var activeCat = null;
var SVGNS = "http://www.w3.org/2000/svg";
var R = 75; // donut radius
var CIRC = 2 * Math.PI * R;
var $ = function (id) { return document.getElementById(id); };
var donut = $("donut");
var segGroup = $("segGroup");
var legendList = $("legendList");
var monthLabel = $("monthLabel");
var centerTotal = $("centerTotal");
var centerDelta = $("centerDelta");
var txnCount = $("txnCount");
var budgetState = $("budgetState");
var topMerchant = $("topMerchant");
var prevBtn = document.querySelector('[data-step="-1"]');
var nextBtn = document.querySelector('[data-step="1"]');
function money(n, withSign) {
var s = "$" + n.toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
return withSign && n > 0 ? "+" + s : s;
}
function total(m) {
return m.cats.reduce(function (sum, c) { return sum + c.amount; }, 0);
}
/* ----------------------------- Toast ----------------------------- */
var toastEl = $("toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2200);
}
/* --------------------------- Highlight --------------------------- */
function setActive(name) {
activeCat = name;
donut.classList.toggle("has-active", !!name);
var segs = segGroup.querySelectorAll(".seg");
for (var i = 0; i < segs.length; i++) {
segs[i].classList.toggle("is-active", segs[i].dataset.cat === name);
}
var rows = legendList.querySelectorAll(".leg");
for (var j = 0; j < rows.length; j++) {
var on = rows[j].dataset.cat === name;
rows[j].classList.toggle("is-active", on);
rows[j].setAttribute("aria-pressed", on ? "true" : "false");
}
var m = MONTHS[current];
if (name) {
var cat = m.cats.find(function (c) { return c.name === name; });
var pct = (cat.amount / total(m)) * 100;
centerTotal.textContent = money(cat.amount).replace(".00", "");
donut.setAttribute("aria-label", name + ": " + money(cat.amount) + ", " + pct.toFixed(1) + "% of spend");
} else {
centerTotal.textContent = money(total(m)).replace(".00", "");
donut.setAttribute("aria-label", "Spending donut chart, total " + money(total(m)));
}
}
function toggleActive(name) {
setActive(activeCat === name ? null : name);
}
/* ----------------------------- Render ---------------------------- */
function render() {
var m = MONTHS[current];
var t = total(m);
monthLabel.textContent = m.label;
prevBtn.disabled = current === 0;
nextBtn.disabled = current === MONTHS.length - 1;
// Center total + month-over-month delta
centerTotal.textContent = money(t).replace(".00", "");
if (current > 0) {
var prevT = total(MONTHS[current - 1]);
var diff = t - prevT;
var pctChange = (diff / prevT) * 100;
centerDelta.textContent =
(diff >= 0 ? "▲ " : "▼ ") + Math.abs(pctChange).toFixed(1) + "% vs last month";
centerDelta.className = "center__delta " + (diff >= 0 ? "up" : "down");
} else {
centerDelta.textContent = "First month on record";
centerDelta.className = "center__delta";
}
// Footer stats
txnCount.textContent = m.txns;
topMerchant.textContent = m.topMerchant;
var over = t > m.budget;
var near = !over && t > m.budget * 0.9;
budgetState.innerHTML = over
? '<span class="pill pill--danger">Over by ' + money(t - m.budget).replace(".00", "") + "</span>"
: near
? '<span class="pill pill--warn">Near limit</span>'
: '<span class="pill pill--ok">On track</span>';
// Sort categories desc so legend + donut read top-down
var cats = m.cats.slice().sort(function (a, b) { return b.amount - a.amount; });
// ---- Donut segments (SVG stroke-dasharray) ----
segGroup.innerHTML = "";
var offset = 0;
cats.forEach(function (c, idx) {
var frac = c.amount / t;
var len = frac * CIRC;
var seg = document.createElementNS(SVGNS, "circle");
seg.setAttribute("class", "seg");
seg.setAttribute("cx", "100");
seg.setAttribute("cy", "100");
seg.setAttribute("r", String(R));
seg.setAttribute("stroke", PALETTE[c.name]);
// gap of 1.2 keeps a hairline between slices
seg.setAttribute("stroke-dasharray", Math.max(0, len - 1.2) + " " + (CIRC - Math.max(0, len - 1.2)));
seg.setAttribute("stroke-dashoffset", String(-offset));
seg.dataset.cat = c.name;
// animate in
seg.style.opacity = "0";
setTimeout(function () { seg.style.opacity = ""; }, 40 + idx * 55);
segGroup.appendChild(seg);
offset += len;
});
// ---- Legend rows ----
legendList.innerHTML = "";
cats.forEach(function (c) {
var pct = (c.amount / t) * 100;
var li = document.createElement("li");
var btn = document.createElement("button");
btn.type = "button";
btn.className = "leg";
btn.dataset.cat = c.name;
btn.setAttribute("aria-pressed", "false");
btn.innerHTML =
'<span class="leg__name"><span class="dot" style="background:' + PALETTE[c.name] + '"></span>' +
'<span>' + c.name + "</span></span>" +
'<span class="leg__amt">' + money(c.amount) + "</span>" +
'<span class="leg__pct">' + pct.toFixed(1) + "%</span>";
li.appendChild(btn);
legendList.appendChild(li);
});
// Re-apply active state if that category still exists this month
if (activeCat && !cats.some(function (c) { return c.name === activeCat; })) {
activeCat = null;
}
setActive(activeCat);
}
/* --------------------------- Interaction ------------------------- */
// Donut hover (preview) + click (sticky toggle)
segGroup.addEventListener("mouseover", function (e) {
var seg = e.target.closest(".seg");
if (seg && !activeCat) setActive(seg.dataset.cat);
});
segGroup.addEventListener("mouseout", function (e) {
var seg = e.target.closest(".seg");
if (seg && !stickyMatches(seg.dataset.cat)) setActive(activeCat);
});
segGroup.addEventListener("click", function (e) {
var seg = e.target.closest(".seg");
if (seg) { stick(seg.dataset.cat); toggleActive(seg.dataset.cat); }
});
// Legend click toggles + hover preview
legendList.addEventListener("click", function (e) {
var row = e.target.closest(".leg");
if (row) { stick(row.dataset.cat); toggleActive(row.dataset.cat); }
});
legendList.addEventListener("mouseover", function (e) {
var row = e.target.closest(".leg");
if (row && !sticky) setActive(row.dataset.cat);
});
legendList.addEventListener("mouseout", function () {
if (!sticky) setActive(activeCat);
});
// "sticky" tracks whether the active state came from a click vs hover
var sticky = null;
function stick(name) { sticky = activeCat === name ? null : name; }
function stickyMatches(name) { return sticky === name; }
// Month switcher
document.querySelector(".month-switch").addEventListener("click", function (e) {
var btn = e.target.closest(".month-btn");
if (!btn) return;
var next = current + Number(btn.dataset.step);
if (next < 0 || next >= MONTHS.length) return;
current = next;
sticky = activeCat; // keep selection across months when possible
render();
toast("Showing " + MONTHS[current].label);
});
// Keyboard: left/right arrows step months
document.addEventListener("keydown", function (e) {
if (e.key === "ArrowLeft" && current > 0) {
current--; render(); toast("Showing " + MONTHS[current].label);
} else if (e.key === "ArrowRight" && current < MONTHS.length - 1) {
current++; render(); toast("Showing " + MONTHS[current].label);
} else if (e.key === "Escape" && activeCat) {
sticky = null; setActive(null);
}
});
$("exportBtn").addEventListener("click", function () {
toast("Statement for " + MONTHS[current].label + " queued for export");
});
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Banking — Spending Breakdown</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="wrap">
<section class="card" aria-labelledby="spend-title">
<header class="card__head">
<div class="head__lead">
<span class="brand" aria-hidden="true">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 21V8l9-5 9 5v13" /><path d="M9 21v-6h6v6" />
</svg>
</span>
<div>
<h1 id="spend-title">Spending breakdown</h1>
<p class="sub">Everyday Checking · <span class="masked">•••• 4242</span></p>
</div>
</div>
<div class="month-switch" role="group" aria-label="Select month">
<button class="month-btn" type="button" data-step="-1" aria-label="Previous month">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg>
</button>
<span class="month-label" id="monthLabel" aria-live="polite">April 2026</span>
<button class="month-btn" type="button" data-step="1" aria-label="Next month">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/></svg>
</button>
</div>
</header>
<div class="body">
<!-- Donut -->
<figure class="chart" aria-hidden="false">
<div class="donut" id="donut" role="img" aria-label="Spending donut chart">
<svg viewBox="0 0 200 200" class="donut__svg" id="donutSvg">
<g transform="rotate(-90 100 100)" id="segGroup"></g>
</svg>
<div class="donut__center">
<span class="center__label">Total spent</span>
<strong class="center__total" id="centerTotal">$0</strong>
<span class="center__delta" id="centerDelta"></span>
</div>
</div>
</figure>
<!-- Legend -->
<div class="legend">
<div class="legend__head">
<span>Category</span>
<span class="ta-r">Amount</span>
<span class="ta-r">Share</span>
</div>
<ul class="legend__list" id="legendList"></ul>
<div class="legend__foot">
<div class="foot__cell">
<span class="foot__k">Transactions</span>
<strong class="foot__v" id="txnCount">0</strong>
</div>
<div class="foot__cell">
<span class="foot__k">vs. budget</span>
<strong class="foot__v" id="budgetState"><span class="pill pill--ok">On track</span></strong>
</div>
<div class="foot__cell">
<span class="foot__k">Top merchant</span>
<strong class="foot__v" id="topMerchant">—</strong>
</div>
</div>
</div>
</div>
<footer class="card__foot">
<span class="secure">
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
Categorised & encrypted
</span>
<button class="link-btn" type="button" id="exportBtn">Export statement</button>
</footer>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Spending Breakdown
A compact spending widget for a banking dashboard. An SVG donut, drawn from stroke-dasharray arcs, splits the selected month’s spend into seven categories — Housing, Groceries, Dining, Transport, Shopping, Subscriptions, and Health — each in its own colour. The running total sits in the centre of the ring in tabular figures, with a small month-over-month delta below it that turns green when spending drops and red when it climbs.
The whole thing is interactive. Hovering a slice previews its category by dimming the rest of the ring and lighting up the matching legend row; clicking a slice or legend row pins that selection so the centre figure swaps to show just that category’s amount and percentage. The legend lists every category with its amount right-aligned and its share of total spend, and a footer summarises the transaction count, a top merchant, and a budget pill that flips between on track, near limit, and over budget.
A pill-style month switcher steps through four months of fictional data — using the arrow buttons or the left/right keys — recomputing the donut, legend, totals, and budget state on each change while keeping your category selection where it still applies. Press Escape to clear a selection. Amounts use font-variant-numeric: tabular-nums so digits never shift, a toast() helper confirms month changes and exports, and the layout collapses to a single column down to 360px.
Illustrative UI only — not real banking software or financial advice.