Dashboard — Single-KPI focus
A focused single-metric dashboard built around one dominant headline number — monthly recurring revenue for a fictional SaaS — with a delta versus the prior period, a large inline-SVG trend chart, and a 7d/30d/90d period selector that swaps the value, recolors the delta, and redraws the line. A row of supporting stats with sparklines, an animated count-up on every change, and a clean whitespace-heavy layout round it out. Pure HTML, CSS, and vanilla JavaScript with no chart libraries.
MCP
Code
:root {
--brand: #5b5bf0;
--brand-d: #4646d6;
--brand-700: #3a3ab8;
--brand-50: #eef0ff;
--accent: #00b4a6;
--accent-soft: #d8f5f2;
--ink: #101322;
--ink-2: #3a4060;
--muted: #6c7393;
--bg: #f6f7fb;
--white: #ffffff;
--surface: #ffffff;
--line: rgba(16, 19, 34, 0.1);
--line-2: rgba(16, 19, 34, 0.16);
--ok: #2f9e6f;
--warn: #d98a2b;
--danger: #d4503e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-sm: 0 1px 2px rgba(16, 19, 34, 0.08);
--sh-lg: 0 8px 24px rgba(16, 19, 34, 0.08);
--sidebar-w: 244px;
}
* {
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
}
h1, h2, h3, p, ul, figure {
margin: 0;
}
ul {
list-style: none;
padding: 0;
}
a {
color: inherit;
text-decoration: none;
}
button {
font: inherit;
cursor: pointer;
}
:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
border-radius: 4px;
}
/* ---------- Shell ---------- */
.shell {
display: grid;
grid-template-columns: var(--sidebar-w) 1fr;
min-height: 100vh;
}
/* ---------- Sidebar ---------- */
.sidebar {
background: var(--white);
border-right: 1px solid var(--line);
padding: 22px 16px;
display: flex;
flex-direction: column;
gap: 26px;
position: sticky;
top: 0;
height: 100vh;
}
.brand {
display: flex;
align-items: center;
gap: 11px;
padding: 0 6px;
}
.brand-mark {
width: 36px;
height: 36px;
display: grid;
place-items: center;
border-radius: 11px;
background: linear-gradient(140deg, var(--brand), var(--brand-700));
color: #fff;
font-weight: 800;
font-size: 1.05rem;
box-shadow: var(--sh-sm);
}
.brand-name {
font-weight: 800;
font-size: 1.06rem;
letter-spacing: -0.01em;
line-height: 1.15;
}
.brand-name em {
display: block;
font-style: normal;
font-weight: 500;
font-size: 0.72rem;
color: var(--muted);
letter-spacing: 0.02em;
}
.nav-list {
display: flex;
flex-direction: column;
gap: 3px;
flex: 1;
}
.nav-link {
display: flex;
align-items: center;
gap: 11px;
padding: 10px 12px;
border-radius: var(--r-sm);
font-weight: 500;
font-size: 0.9rem;
color: var(--ink-2);
transition: background 0.15s, color 0.15s;
}
.nav-link .ni {
width: 22px;
text-align: center;
font-size: 0.95rem;
color: var(--muted);
}
.nav-link:hover {
background: var(--bg);
color: var(--ink);
}
.nav-link.is-active {
background: var(--brand-50);
color: var(--brand-d);
font-weight: 600;
}
.nav-link.is-active .ni {
color: var(--brand-d);
}
.nav-foot {
border-top: 1px solid var(--line);
padding-top: 16px;
}
.who {
display: flex;
align-items: center;
gap: 11px;
}
.avatar {
width: 38px;
height: 38px;
border-radius: 50%;
display: grid;
place-items: center;
background: var(--accent-soft);
color: #0a6b63;
font-weight: 700;
font-size: 0.82rem;
}
.who-meta {
display: flex;
flex-direction: column;
line-height: 1.25;
}
.who-meta strong {
font-size: 0.87rem;
}
.who-meta small {
color: var(--muted);
font-size: 0.74rem;
}
/* ---------- Main wrap ---------- */
.main-wrap {
display: flex;
flex-direction: column;
min-width: 0;
}
.topbar {
display: flex;
align-items: center;
gap: 16px;
padding: 18px 32px;
border-bottom: 1px solid var(--line);
background: rgba(246, 247, 251, 0.86);
backdrop-filter: blur(8px);
position: sticky;
top: 0;
z-index: 20;
}
.icon-btn {
border: 1px solid var(--line);
background: var(--white);
width: 40px;
height: 40px;
border-radius: var(--r-sm);
font-size: 1.1rem;
color: var(--ink-2);
}
.menu-toggle {
display: none;
}
.head-titles {
flex: 1;
min-width: 0;
}
.eyebrow {
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--accent);
margin-bottom: 2px;
}
.head-titles h1 {
font-size: 1.28rem;
font-weight: 700;
letter-spacing: -0.02em;
}
.head-tools {
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
}
/* Segmented control */
.seg {
display: inline-flex;
background: var(--white);
border: 1px solid var(--line);
border-radius: 10px;
padding: 3px;
gap: 2px;
box-shadow: var(--sh-sm);
}
.seg-btn {
border: 0;
background: transparent;
color: var(--muted);
font-weight: 600;
font-size: 0.84rem;
padding: 7px 16px;
border-radius: 7px;
transition: background 0.15s, color 0.15s;
}
.seg-btn:hover {
color: var(--ink);
}
.seg-btn.is-active {
background: var(--brand);
color: #fff;
box-shadow: var(--sh-sm);
}
/* Live toggle */
.live-toggle {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.84rem;
font-weight: 600;
color: var(--ink-2);
cursor: pointer;
user-select: none;
}
.live-toggle input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.live-track {
width: 38px;
height: 22px;
border-radius: 999px;
background: var(--line-2);
position: relative;
transition: background 0.18s;
flex: none;
}
.live-dot {
position: absolute;
top: 3px;
left: 3px;
width: 16px;
height: 16px;
border-radius: 50%;
background: #fff;
box-shadow: var(--sh-sm);
transition: transform 0.18s;
}
.live-toggle input:checked + .live-track {
background: var(--accent);
}
.live-toggle input:checked + .live-track .live-dot {
transform: translateX(16px);
}
.live-toggle input:focus-visible + .live-track {
outline: 2px solid var(--brand);
outline-offset: 2px;
}
/* ---------- Content ---------- */
.content {
padding: 32px;
display: flex;
flex-direction: column;
gap: 26px;
max-width: 1180px;
width: 100%;
margin: 0 auto;
}
/* ---------- Hero ---------- */
.hero {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-lg);
padding: 36px 40px 28px;
}
.hero-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 24px;
flex-wrap: wrap;
}
.hero-eyebrow {
font-size: 0.82rem;
font-weight: 600;
color: var(--muted);
margin-bottom: 6px;
}
.hero-eyebrow span {
color: var(--ink-2);
}
.hero-figure {
display: flex;
align-items: flex-start;
line-height: 1;
}
.hero-currency {
font-size: 2.4rem;
font-weight: 600;
color: var(--muted);
margin-top: 8px;
}
.hero-value {
font-size: clamp(3.4rem, 8vw, 5.4rem);
font-weight: 800;
letter-spacing: -0.04em;
color: var(--ink);
font-variant-numeric: tabular-nums;
}
.hero-sub {
margin-top: 10px;
color: var(--ink-2);
font-size: 0.94rem;
max-width: 42ch;
}
.hero-delta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 6px;
text-align: right;
padding-top: 6px;
}
.delta-chip {
display: inline-flex;
align-items: center;
gap: 6px;
font-weight: 700;
font-size: 1.04rem;
padding: 6px 12px;
border-radius: 999px;
}
.delta-chip.up {
color: var(--ok);
background: rgba(47, 158, 111, 0.12);
}
.delta-chip.down {
color: var(--danger);
background: rgba(212, 80, 62, 0.12);
}
.delta-arrow {
font-size: 0.78rem;
}
.delta-note {
font-size: 0.78rem;
color: var(--muted);
}
.delta-abs {
font-size: 0.86rem;
font-weight: 600;
color: var(--ink-2);
font-variant-numeric: tabular-nums;
}
/* ---------- Trend chart ---------- */
.trend {
margin-top: 28px;
}
.trend-cap {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.trend-title {
font-weight: 600;
font-size: 0.9rem;
color: var(--ink-2);
}
.trend-legend {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 0.8rem;
color: var(--muted);
}
.swatch {
width: 12px;
height: 12px;
border-radius: 4px;
background: var(--brand);
}
.trend-canvas {
position: relative;
height: 280px;
}
#trendSvg {
width: 100%;
height: 100%;
display: block;
overflow: visible;
}
.grid-line {
stroke: var(--line);
stroke-width: 1;
}
.y-label {
fill: var(--muted);
font-size: 12px;
font-weight: 500;
}
.x-label {
fill: var(--muted);
font-size: 12px;
font-weight: 500;
}
.area-fill {
transition: d 0.45s ease;
}
.line-path {
stroke: var(--brand);
stroke-width: 2.6;
stroke-linecap: round;
stroke-linejoin: round;
filter: drop-shadow(0 6px 10px rgba(91, 91, 240, 0.22));
}
.cross-line {
stroke: var(--brand-d);
stroke-width: 1;
stroke-dasharray: 4 4;
}
.cross-dot {
fill: var(--white);
stroke: var(--brand);
stroke-width: 3;
}
.trend-tip {
position: absolute;
transform: translate(-50%, -120%);
background: var(--ink);
color: #fff;
padding: 7px 11px;
border-radius: var(--r-sm);
box-shadow: var(--sh-lg);
pointer-events: none;
white-space: nowrap;
display: flex;
flex-direction: column;
line-height: 1.25;
z-index: 5;
}
.trend-tip strong {
font-size: 0.95rem;
font-variant-numeric: tabular-nums;
}
.trend-tip small {
font-size: 0.72rem;
opacity: 0.75;
}
.trend-tip::after {
content: "";
position: absolute;
left: 50%;
bottom: -5px;
width: 10px;
height: 10px;
background: var(--ink);
transform: translateX(-50%) rotate(45deg);
}
/* ---------- Supporting stats ---------- */
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 18px;
}
.stat-card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
box-shadow: var(--sh-sm);
padding: 18px 18px 14px;
display: flex;
flex-direction: column;
gap: 12px;
transition: transform 0.16s, box-shadow 0.16s, border-color 0.16s;
}
.stat-card:hover {
transform: translateY(-3px);
box-shadow: var(--sh-lg);
border-color: var(--line-2);
}
.stat-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.stat-label {
font-size: 0.82rem;
font-weight: 600;
color: var(--muted);
}
.dots {
border: 0;
background: transparent;
color: var(--muted);
font-size: 1.1rem;
line-height: 1;
padding: 2px 6px;
border-radius: var(--r-sm);
}
.dots:hover {
background: var(--bg);
color: var(--ink);
}
.stat-row {
display: flex;
align-items: baseline;
gap: 10px;
flex-wrap: wrap;
}
.stat-value {
font-size: 1.55rem;
font-weight: 800;
letter-spacing: -0.02em;
color: var(--ink);
font-variant-numeric: tabular-nums;
}
.stat-delta {
font-size: 0.8rem;
font-weight: 700;
}
.stat-delta.up {
color: var(--ok);
}
.stat-delta.down {
color: var(--danger);
}
.spark {
width: 100%;
height: 36px;
display: block;
}
.spark-line {
fill: none;
stroke: var(--brand);
stroke-width: 2.2;
stroke-linecap: round;
stroke-linejoin: round;
}
.stat-card[data-stat="churn"] .spark-line {
stroke: var(--danger);
}
/* ---------- Scrim ---------- */
.scrim {
display: none;
position: fixed;
inset: 0;
background: rgba(16, 19, 34, 0.42);
z-index: 30;
}
.scrim.show {
display: block;
}
/* ---------- Toast ---------- */
.toast-wrap {
position: fixed;
right: 20px;
bottom: 20px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 60;
}
.toast {
background: var(--ink);
color: #fff;
padding: 11px 16px;
border-radius: var(--r-sm);
box-shadow: var(--sh-lg);
font-size: 0.86rem;
font-weight: 500;
opacity: 0;
transform: translateY(10px);
transition: opacity 0.2s, transform 0.2s;
}
.toast.show {
opacity: 1;
transform: translateY(0);
}
/* ---------- Responsive ---------- */
@media (max-width: 980px) {
.stats {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 720px) {
.shell {
grid-template-columns: 1fr;
}
.sidebar {
position: fixed;
top: 0;
left: 0;
width: 264px;
z-index: 40;
transform: translateX(-100%);
transition: transform 0.24s ease;
}
.sidebar.open {
transform: translateX(0);
box-shadow: var(--sh-lg);
}
.menu-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
}
.topbar {
padding: 14px 18px;
}
.content {
padding: 20px 16px;
}
.hero {
padding: 24px 22px 20px;
}
.hero-head {
flex-direction: column;
}
.hero-delta {
align-items: flex-start;
text-align: left;
}
}
@media (max-width: 460px) {
.stats {
grid-template-columns: 1fr;
}
.head-tools {
width: 100%;
justify-content: space-between;
}
.trend-canvas {
height: 220px;
}
}/* Northwind Cloud — Single-KPI dashboard
Vanilla JS only. Period selector swaps the headline MRR, delta and trend line,
with an animated count-up. A live toggle nudges the current value periodically. */
(function () {
"use strict";
/* ---------- Fictional datasets ---------- */
// Each period: a trend series (MRR by point), its x-axis labels, the previous
// period total for delta, and the supporting stat figures.
function gen(seed, n, base, drift, noise) {
var arr = [];
var v = base;
var s = seed;
for (var i = 0; i < n; i++) {
// deterministic pseudo-random so the chart is stable per period
s = (s * 9301 + 49297) % 233280;
var r = s / 233280;
v += drift + (r - 0.45) * noise;
arr.push(Math.max(0, Math.round(v)));
}
return arr;
}
var DATA = {
"7d": {
label: "last 7 days",
series: gen(7, 7, 372000, 1900, 5200),
x: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
prev: 368400,
summary: "Steady week — recurring revenue up across self-serve and sales-led plans.",
stats: {
customers: { value: 71, delta: "▲ 6.1%", dir: "up", spark: [40, 44, 42, 51, 49, 58, 71] },
churn: { value: "1.7%", delta: "▼ 0.2pp", dir: "down", spark: [26, 24, 25, 22, 21, 20, 17] },
arpu: { value: "$57.90", delta: "▲ 1.2%", dir: "up", spark: [52, 53, 54, 54, 55, 56, 58] },
expansion: { value: "$4.1k", delta: "▲ 9.0%", dir: "up", spark: [18, 22, 20, 28, 26, 34, 41] }
}
},
"30d": {
label: "last 30 days",
series: gen(30, 30, 351000, 880, 6400),
x: ["W1", "", "", "", "", "W2", "", "", "", "", "W3", "", "", "", "", "W4", "", "", "", "", "W5", "", "", "", "", "", "", "", "", "Now"],
prev: 339600,
summary: "Strong month driven by expansion seats on the Scale tier and lower churn.",
stats: {
customers: { value: 312, delta: "▲ 8.4%", dir: "up", spark: [180, 205, 198, 240, 235, 280, 312] },
churn: { value: "1.9%", delta: "▼ 0.3pp", dir: "down", spark: [30, 28, 29, 25, 24, 22, 19] },
arpu: { value: "$58.20", delta: "▲ 2.1%", dir: "up", spark: [50, 52, 53, 55, 56, 57, 58] },
expansion: { value: "$14.6k", delta: "▲ 12.0%", dir: "up", spark: [60, 72, 70, 95, 92, 120, 146] }
}
},
"90d": {
label: "last 90 days",
series: gen(90, 90, 298000, 1050, 7800),
x: ["Mar", "", "", "", "", "", "", "", "", "Apr", "", "", "", "", "", "", "", "", "May", "", "", "", "", "", "", "", "", "Jun", "", "",
"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
prev: 271200,
summary: "Quarter-over-quarter growth steady at double digits with falling churn.",
stats: {
customers: { value: 904, delta: "▲ 14.2%", dir: "up", spark: [520, 600, 590, 690, 700, 820, 904] },
churn: { value: "2.1%", delta: "▼ 0.5pp", dir: "down", spark: [34, 32, 30, 28, 26, 24, 21] },
arpu: { value: "$56.40", delta: "▲ 3.4%", dir: "up", spark: [46, 48, 50, 52, 53, 55, 56] },
expansion: { value: "$41.2k", delta: "▲ 18.5%", dir: "up", spark: [120, 160, 150, 220, 240, 320, 412] }
}
}
};
// chart geometry
var VB = { w: 920, h: 320, padL: 56, padR: 16, padT: 18, padB: 30 };
var state = { period: "7d", live: true, timer: null, raf: null };
/* ---------- DOM ---------- */
var $ = function (s, r) { return (r || document).querySelector(s); };
var $$ = function (s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); };
var heroValue = $("#heroValue");
var periodLabel = $("#periodLabel");
var heroSummary = $("#heroSummary");
var deltaChip = $("#deltaChip");
var deltaArrow = $("#deltaArrow");
var deltaPct = $("#deltaPct");
var deltaAbs = $("#deltaAbs");
var svg = $("#trendSvg");
var tip = $("#trendTip");
var tipValue = $("#tipValue");
var tipLabel = $("#tipLabel");
var SVGNS = "http://www.w3.org/2000/svg";
/* ---------- Formatting ---------- */
function fmtMoney(n) {
return Math.round(n).toLocaleString("en-US");
}
function fmtCompact(n) {
if (n >= 1000) return "$" + (n / 1000).toFixed(1) + "k";
return "$" + Math.round(n);
}
/* ---------- Count-up animation ---------- */
function animateValue(el, from, to, dur) {
if (state.raf) cancelAnimationFrame(state.raf);
var start = null;
function ease(t) { return 1 - Math.pow(1 - t, 3); }
function step(ts) {
if (start === null) start = ts;
var p = Math.min(1, (ts - start) / dur);
var cur = from + (to - from) * ease(p);
el.textContent = fmtMoney(cur);
if (p < 1) state.raf = requestAnimationFrame(step);
else { el.textContent = fmtMoney(to); el.dataset.value = String(to); }
}
state.raf = requestAnimationFrame(step);
}
/* ---------- SVG helpers ---------- */
function makeEl(name, attrs) {
var el = document.createElementNS(SVGNS, name);
for (var k in attrs) el.setAttribute(k, attrs[k]);
return el;
}
// map a series to screen coordinates
function project(series) {
var min = Math.min.apply(null, series);
var max = Math.max.apply(null, series);
var range = max - min || 1;
// pad the y-domain a touch so the line never hugs the edges
var lo = min - range * 0.12;
var hi = max + range * 0.12;
var w = VB.w - VB.padL - VB.padR;
var h = VB.h - VB.padT - VB.padB;
var pts = series.map(function (v, i) {
var x = VB.padL + (series.length === 1 ? 0 : (i / (series.length - 1)) * w);
var y = VB.padT + (1 - (v - lo) / (hi - lo)) * h;
return { x: x, y: y, v: v };
});
return { pts: pts, lo: lo, hi: hi };
}
// smooth path (Catmull-Rom -> cubic bezier)
function smoothPath(pts) {
if (pts.length < 2) return "";
var d = "M" + pts[0].x.toFixed(1) + " " + pts[0].y.toFixed(1);
for (var i = 0; i < pts.length - 1; i++) {
var p0 = pts[i - 1] || pts[i];
var p1 = pts[i];
var p2 = pts[i + 1];
var p3 = pts[i + 2] || p2;
var c1x = p1.x + (p2.x - p0.x) / 6;
var c1y = p1.y + (p2.y - p0.y) / 6;
var c2x = p2.x - (p3.x - p1.x) / 6;
var c2y = p2.y - (p3.y - p1.y) / 6;
d += " C" + c1x.toFixed(1) + " " + c1y.toFixed(1) + " " +
c2x.toFixed(1) + " " + c2y.toFixed(1) + " " +
p2.x.toFixed(1) + " " + p2.y.toFixed(1);
}
return d;
}
var lastPts = [];
function drawTrend(d) {
while (svg.firstChild) svg.removeChild(svg.firstChild);
var proj = project(d.series);
lastPts = proj.pts;
// gradient def for the area fill
var defs = makeEl("defs", {});
var grad = makeEl("linearGradient", { id: "areaGrad", x1: "0", y1: "0", x2: "0", y2: "1" });
grad.appendChild(makeEl("stop", { offset: "0%", "stop-color": "#5b5bf0", "stop-opacity": "0.26" }));
grad.appendChild(makeEl("stop", { offset: "100%", "stop-color": "#5b5bf0", "stop-opacity": "0" }));
defs.appendChild(grad);
svg.appendChild(defs);
// y gridlines + labels (5 rows)
var rows = 4;
for (var r = 0; r <= rows; r++) {
var t = r / rows;
var y = VB.padT + t * (VB.h - VB.padT - VB.padB);
svg.appendChild(makeEl("line", {
class: "grid-line", x1: VB.padL, x2: VB.w - VB.padR, y1: y.toFixed(1), y2: y.toFixed(1)
}));
var val = proj.hi - t * (proj.hi - proj.lo);
var lbl = makeEl("text", { class: "y-label", x: VB.padL - 10, y: (y + 4).toFixed(1), "text-anchor": "end" });
lbl.textContent = "$" + (val / 1000).toFixed(0) + "k";
svg.appendChild(lbl);
}
// x labels (only non-empty)
var w = VB.w - VB.padL - VB.padR;
d.x.forEach(function (lab, i) {
if (!lab) return;
var x = VB.padL + (d.x.length === 1 ? 0 : (i / (d.x.length - 1)) * w);
var t = makeEl("text", { class: "x-label", x: x.toFixed(1), y: VB.h - 8, "text-anchor": "middle" });
t.textContent = lab;
svg.appendChild(t);
});
// area fill
var areaD = smoothPath(proj.pts) +
" L" + proj.pts[proj.pts.length - 1].x.toFixed(1) + " " + (VB.h - VB.padB) +
" L" + proj.pts[0].x.toFixed(1) + " " + (VB.h - VB.padB) + " Z";
svg.appendChild(makeEl("path", { class: "area-fill", d: areaD, fill: "url(#areaGrad)" }));
// line with draw-in animation
var lineD = smoothPath(proj.pts);
var line = makeEl("path", { class: "line-path", d: lineD });
svg.appendChild(line);
var len = line.getTotalLength ? line.getTotalLength() : 0;
if (len) {
line.style.strokeDasharray = len;
line.style.strokeDashoffset = len;
line.getBoundingClientRect(); // reflow
line.style.transition = "stroke-dashoffset 0.7s ease";
line.style.strokeDashoffset = "0";
}
// endpoint dot
var end = proj.pts[proj.pts.length - 1];
svg.appendChild(makeEl("circle", { class: "cross-dot", cx: end.x, cy: end.y, r: 4.5 }));
// crosshair group (hidden until hover)
var cross = makeEl("g", { id: "crossGroup", style: "opacity:0" });
cross.appendChild(makeEl("line", { id: "crossLine", class: "cross-line", y1: VB.padT, y2: VB.h - VB.padB }));
cross.appendChild(makeEl("circle", { id: "crossDot", class: "cross-dot", r: 5.5 }));
svg.appendChild(cross);
// transparent hit rect for hover
svg.appendChild(makeEl("rect", { x: 0, y: 0, width: VB.w, height: VB.h, fill: "transparent", id: "hit" }));
var hit = $("#hit", svg);
hit.addEventListener("mousemove", onHover);
hit.addEventListener("mouseleave", hideTip);
hit.addEventListener("touchmove", function (e) {
if (e.touches[0]) onHover(e.touches[0]);
}, { passive: true });
}
function onHover(e) {
if (!lastPts.length) return;
var rect = svg.getBoundingClientRect();
var px = ((e.clientX - rect.left) / rect.width) * VB.w;
// nearest point
var best = lastPts[0], bi = 0;
for (var i = 1; i < lastPts.length; i++) {
if (Math.abs(lastPts[i].x - px) < Math.abs(best.x - px)) { best = lastPts[i]; bi = i; }
}
var cross = $("#crossGroup", svg);
var line = $("#crossLine", svg);
var dot = $("#crossDot", svg);
if (cross) cross.style.opacity = "1";
if (line) { line.setAttribute("x1", best.x); line.setAttribute("x2", best.x); }
if (dot) { dot.setAttribute("cx", best.x); dot.setAttribute("cy", best.y); }
tip.hidden = false;
tipValue.textContent = "$" + fmtMoney(best.v);
var xl = DATA[state.period].x[bi] || ("Point " + (bi + 1));
tipLabel.textContent = xl;
tip.style.left = (best.x / VB.w) * rect.width + "px";
tip.style.top = (best.y / VB.h) * rect.height + "px";
}
function hideTip() {
tip.hidden = true;
var cross = $("#crossGroup", svg);
if (cross) cross.style.opacity = "0";
}
/* ---------- Sparklines ---------- */
function drawSpark(path, vals) {
var w = 120, h = 36, pad = 3;
var min = Math.min.apply(null, vals);
var max = Math.max.apply(null, vals);
var range = max - min || 1;
var d = vals.map(function (v, i) {
var x = pad + (i / (vals.length - 1)) * (w - pad * 2);
var y = pad + (1 - (v - min) / range) * (h - pad * 2);
return (i === 0 ? "M" : "L") + x.toFixed(1) + " " + y.toFixed(1);
}).join(" ");
path.setAttribute("d", d);
}
/* ---------- Render a period ---------- */
function render(period) {
var d = DATA[period];
state.period = period;
var total = d.series[d.series.length - 1];
var from = parseFloat(heroValue.dataset.value || "0");
animateValue(heroValue, from, total, 700);
periodLabel.textContent = d.label;
heroSummary.textContent = d.summary;
var diff = total - d.prev;
var pct = (diff / d.prev) * 100;
var up = diff >= 0;
deltaChip.classList.toggle("up", up);
deltaChip.classList.toggle("down", !up);
deltaArrow.textContent = up ? "▲" : "▼";
deltaPct.textContent = Math.abs(pct).toFixed(1) + "%";
deltaAbs.textContent = (up ? "+$" : "−$") + fmtMoney(Math.abs(diff));
drawTrend(d);
// supporting stats
$$(".stat-card").forEach(function (card) {
var key = card.dataset.stat;
var s = d.stats[key];
if (!s) return;
$(".stat-value", card).textContent = s.value;
var dl = $(".stat-delta", card);
dl.textContent = s.delta;
dl.className = "stat-delta " + s.dir;
drawSpark($(".spark-line", card), s.spark);
});
}
/* ---------- Live tick ---------- */
function startLive() {
stopLive();
state.timer = setInterval(function () {
var d = DATA[state.period];
var last = d.series[d.series.length - 1];
// small ± nudge, biased slightly up
var nudge = Math.round((Math.random() - 0.42) * (last * 0.0016));
d.series[d.series.length - 1] = Math.max(0, last + nudge);
var from = parseFloat(heroValue.dataset.value || "0");
var to = d.series[d.series.length - 1];
animateValue(heroValue, from, to, 450);
// recompute delta + redraw quietly
var diff = to - d.prev;
var pct = (diff / d.prev) * 100;
var up = diff >= 0;
deltaChip.classList.toggle("up", up);
deltaChip.classList.toggle("down", !up);
deltaArrow.textContent = up ? "▲" : "▼";
deltaPct.textContent = Math.abs(pct).toFixed(1) + "%";
deltaAbs.textContent = (up ? "+$" : "−$") + fmtMoney(Math.abs(diff));
drawTrend(d);
}, 3200);
}
function stopLive() {
if (state.timer) { clearInterval(state.timer); state.timer = null; }
}
/* ---------- Toast ---------- */
var toastWrap = $("#toastWrap");
function toast(msg) {
var t = document.createElement("div");
t.className = "toast";
t.textContent = msg;
toastWrap.appendChild(t);
requestAnimationFrame(function () { t.classList.add("show"); });
setTimeout(function () {
t.classList.remove("show");
setTimeout(function () { t.remove(); }, 220);
}, 2400);
}
/* ---------- Wire up ---------- */
$$(".seg-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
if (btn.classList.contains("is-active")) return;
$$(".seg-btn").forEach(function (b) {
b.classList.remove("is-active");
b.setAttribute("aria-pressed", "false");
});
btn.classList.add("is-active");
btn.setAttribute("aria-pressed", "true");
render(btn.dataset.period);
if (state.live) startLive();
toast("Showing " + DATA[btn.dataset.period].label);
});
});
var liveToggle = $("#liveToggle");
liveToggle.addEventListener("change", function () {
state.live = liveToggle.checked;
if (state.live) { startLive(); toast("Live updates on"); }
else { stopLive(); toast("Live updates paused"); }
});
// mobile nav
var menuToggle = $("#menuToggle");
var sidebar = $(".sidebar");
var scrim = $("#scrim");
function openNav() {
sidebar.classList.add("open");
scrim.classList.add("show");
scrim.hidden = false;
menuToggle.setAttribute("aria-expanded", "true");
}
function closeNav() {
sidebar.classList.remove("open");
scrim.classList.remove("show");
scrim.hidden = true;
menuToggle.setAttribute("aria-expanded", "false");
}
menuToggle.addEventListener("click", function () {
sidebar.classList.contains("open") ? closeNav() : openNav();
});
scrim.addEventListener("click", closeNav);
$$(".nav-link").forEach(function (l) {
l.addEventListener("click", function (e) {
e.preventDefault();
$$(".nav-link").forEach(function (n) { n.classList.remove("is-active"); n.removeAttribute("aria-current"); });
l.classList.add("is-active");
l.setAttribute("aria-current", "page");
closeNav();
});
});
// widget menus
$$(".dots").forEach(function (b) {
b.addEventListener("click", function () { toast("Widget options coming soon"); });
});
document.addEventListener("keydown", function (e) {
if (e.key === "Escape") closeNav();
});
// redraw on resize so tips/positions stay aligned
var rt;
window.addEventListener("resize", function () {
clearTimeout(rt);
rt = setTimeout(function () { drawTrend(DATA[state.period]); }, 150);
});
// initial paint
render("7d");
startLive();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Northwind Cloud — Revenue focus</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>
<div class="shell">
<!-- Sidebar -->
<nav class="sidebar" aria-label="Primary">
<div class="brand">
<span class="brand-mark" aria-hidden="true">N</span>
<span class="brand-name">Northwind<em>Cloud</em></span>
</div>
<ul class="nav-list">
<li><a href="#" class="nav-link is-active" aria-current="page"><span class="ni" aria-hidden="true">◧</span> Overview</a></li>
<li><a href="#" class="nav-link"><span class="ni" aria-hidden="true">▤</span> Revenue</a></li>
<li><a href="#" class="nav-link"><span class="ni" aria-hidden="true">◔</span> Customers</a></li>
<li><a href="#" class="nav-link"><span class="ni" aria-hidden="true">◇</span> Plans</a></li>
<li><a href="#" class="nav-link"><span class="ni" aria-hidden="true">⚙</span> Settings</a></li>
</ul>
<div class="nav-foot">
<div class="who">
<span class="avatar" aria-hidden="true">AK</span>
<span class="who-meta"><strong>Avery Kline</strong><small>Finance</small></span>
</div>
</div>
</nav>
<div class="main-wrap">
<!-- Topbar -->
<header class="topbar">
<button class="icon-btn menu-toggle" id="menuToggle" aria-label="Toggle navigation" aria-expanded="false">☰</button>
<div class="head-titles">
<p class="eyebrow">Finance · live</p>
<h1>Monthly recurring revenue</h1>
</div>
<div class="head-tools">
<div class="seg" role="group" aria-label="Select period">
<button class="seg-btn is-active" data-period="7d" aria-pressed="true">7d</button>
<button class="seg-btn" data-period="30d" aria-pressed="false">30d</button>
<button class="seg-btn" data-period="90d" aria-pressed="false">90d</button>
</div>
<label class="live-toggle">
<input type="checkbox" id="liveToggle" checked />
<span class="live-track" aria-hidden="true"><span class="live-dot"></span></span>
<span class="live-label">Live</span>
</label>
</div>
</header>
<main class="content" aria-labelledby="heroLabel">
<!-- HERO single KPI -->
<section class="hero" aria-live="polite">
<div class="hero-head">
<div>
<p class="hero-eyebrow" id="heroLabel">MRR · <span id="periodLabel">last 7 days</span></p>
<div class="hero-figure">
<span class="hero-currency">$</span>
<span class="hero-value" id="heroValue" data-value="0">0</span>
</div>
<p class="hero-sub" id="heroSummary">—</p>
</div>
<div class="hero-delta">
<span class="delta-chip up" id="deltaChip">
<span class="delta-arrow" id="deltaArrow" aria-hidden="true">▲</span>
<span id="deltaPct">0%</span>
</span>
<span class="delta-note" id="deltaNote">vs previous period</span>
<span class="delta-abs" id="deltaAbs">+$0</span>
</div>
</div>
<!-- big trend chart -->
<figure class="trend" aria-label="Revenue trend chart">
<figcaption class="trend-cap">
<span class="trend-title">Trend</span>
<span class="trend-legend"><span class="swatch"></span> Recurring revenue</span>
</figcaption>
<div class="trend-canvas" id="trendCanvas">
<svg id="trendSvg" viewBox="0 0 920 320" preserveAspectRatio="none" role="img" aria-label="Line chart of recurring revenue over the selected period"></svg>
<div class="trend-tip" id="trendTip" hidden>
<strong id="tipValue">$0</strong>
<small id="tipLabel">—</small>
</div>
</div>
</figure>
</section>
<!-- supporting stats -->
<section class="stats" aria-label="Supporting statistics">
<article class="stat-card" data-stat="customers">
<header class="stat-head">
<span class="stat-label">New customers</span>
<button class="dots" aria-label="More options">⋯</button>
</header>
<div class="stat-row">
<span class="stat-value">312</span>
<span class="stat-delta up">▲ 8.4%</span>
</div>
<svg class="spark" viewBox="0 0 120 36" preserveAspectRatio="none" aria-hidden="true"><path class="spark-line" d=""/></svg>
</article>
<article class="stat-card" data-stat="churn">
<header class="stat-head">
<span class="stat-label">Net churn</span>
<button class="dots" aria-label="More options">⋯</button>
</header>
<div class="stat-row">
<span class="stat-value">1.9%</span>
<span class="stat-delta down">▼ 0.3pp</span>
</div>
<svg class="spark" viewBox="0 0 120 36" preserveAspectRatio="none" aria-hidden="true"><path class="spark-line" d=""/></svg>
</article>
<article class="stat-card" data-stat="arpu">
<header class="stat-head">
<span class="stat-label">ARPU</span>
<button class="dots" aria-label="More options">⋯</button>
</header>
<div class="stat-row">
<span class="stat-value">$58.20</span>
<span class="stat-delta up">▲ 2.1%</span>
</div>
<svg class="spark" viewBox="0 0 120 36" preserveAspectRatio="none" aria-hidden="true"><path class="spark-line" d=""/></svg>
</article>
<article class="stat-card" data-stat="expansion">
<header class="stat-head">
<span class="stat-label">Expansion MRR</span>
<button class="dots" aria-label="More options">⋯</button>
</header>
<div class="stat-row">
<span class="stat-value">$14.6k</span>
<span class="stat-delta up">▲ 12.0%</span>
</div>
<svg class="spark" viewBox="0 0 120 36" preserveAspectRatio="none" aria-hidden="true"><path class="spark-line" d=""/></svg>
</article>
</section>
</main>
</div>
</div>
<div class="scrim" id="scrim" hidden></div>
<div class="toast-wrap" id="toastWrap" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Single-KPI focus
A deliberately calm dashboard that puts one metric front and centre. The hero panel shows the monthly recurring revenue for Northwind Cloud as a huge headline figure, paired with a delta chip that turns green or red versus the previous period and a one-line plain-language summary. Beneath it sits a large area + line trend chart drawn entirely with inline SVG — gradient fill, smooth path, gridlines, axis labels, and a hover crosshair that reads out the value at any point.
The 7d / 30d / 90d period selector is the primary interaction. Clicking a range swaps the dataset: the headline number animates by counting up or down to its new value, the delta chip recomputes and recolors, the summary copy updates, and the trend line redraws to fit the new window. A small live-toggle nudges the current value every few seconds so the dashboard feels alive, and the supporting stat strip — new customers, churn, ARPU, expansion — each carry their own mini sparkline and up/down delta.
Everything is responsive: the supporting stats reflow from four columns to two to one, the chart scales to its container, and the header filters wrap on narrow screens down to about 360px. Controls are keyboard-operable with visible focus rings, the regions use proper landmark roles, and the whole thing stays within a neutral product-UI palette with generous whitespace so the single metric never has to compete for attention.