Podcast — Stats Dashboard
A dark, audio-first creator analytics dashboard with KPI cards for downloads, subscribers and average listen-through, an animated inline-SVG bar chart of downloads per episode, a sortable top-episodes table with trend badges, and a platform breakdown donut. Range toggles, animated count-up figures, hover tooltips, keyboard-accessible controls and a toast helper make it feel like a real podcast host backend — fully self-contained vanilla HTML, CSS and JS.
MCP
Code
:root {
--bg: #0c0c10;
--surface: #15151c;
--surface-2: #1d1d27;
--violet: #8b5cf6;
--violet-d: #7c3aed;
--cyan: #22d3ee;
--pink: #f472b6;
--ink: #f4f4f8;
--muted: #9a9aab;
--line: rgba(255, 255, 255, 0.10);
--line-2: rgba(255, 255, 255, 0.18);
--white: #fff;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--pos: #34d399;
--neg: #fb7185;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(1100px 520px at 12% -8%, rgba(139, 92, 246, 0.18), transparent 60%),
radial-gradient(900px 480px at 100% 0%, rgba(34, 211, 238, 0.12), 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;
}
h1, h2, p { margin: 0; }
.app {
max-width: 1160px;
margin: 0 auto;
padding: 28px 24px 56px;
}
/* ---------- Top bar ---------- */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
flex-wrap: wrap;
margin-bottom: 26px;
}
.brand { display: flex; align-items: center; gap: 14px; }
.brand__logo {
width: 48px;
height: 48px;
border-radius: var(--r-md);
display: grid;
place-items: center;
background: linear-gradient(140deg, var(--violet-d), var(--cyan));
box-shadow: 0 8px 26px rgba(124, 58, 237, 0.45);
}
.wave { display: flex; align-items: center; gap: 3px; height: 20px; }
.wave i {
width: 3px;
border-radius: 3px;
background: var(--white);
animation: wave 1.1s ease-in-out infinite;
}
.wave i:nth-child(1) { height: 8px; animation-delay: 0s; }
.wave i:nth-child(2) { height: 18px; animation-delay: .15s; }
.wave i:nth-child(3) { height: 12px; animation-delay: .3s; }
.wave i:nth-child(4) { height: 20px; animation-delay: .45s; }
.wave i:nth-child(5) { height: 9px; animation-delay: .6s; }
@keyframes wave { 0%, 100% { transform: scaleY(.5); } 50% { transform: scaleY(1); } }
.brand__meta h1 { font-size: 1.25rem; font-weight: 800; letter-spacing: -0.02em; }
.brand__meta p {
font-size: .8rem;
color: var(--muted);
display: flex;
align-items: center;
gap: 7px;
}
.dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--pos);
box-shadow: 0 0 0 0 rgba(52, 211, 153, .5);
animation: pulse 1.8s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(52, 211, 153, .5); }
70% { box-shadow: 0 0 0 7px rgba(52, 211, 153, 0); }
100% { box-shadow: 0 0 0 0 rgba(52, 211, 153, 0); }
}
.topbar__actions { display: flex; align-items: center; gap: 12px; }
.range {
display: inline-flex;
padding: 4px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 999px;
}
.range__btn {
border: 0;
background: transparent;
color: var(--muted);
font: inherit;
font-weight: 600;
font-size: .82rem;
padding: 7px 15px;
border-radius: 999px;
cursor: pointer;
transition: color .2s, background .2s;
}
.range__btn:hover { color: var(--ink); }
.range__btn.is-active {
color: var(--white);
background: linear-gradient(135deg, var(--violet-d), var(--violet));
box-shadow: 0 4px 16px rgba(124, 58, 237, .4);
}
.btn {
border: 1px solid var(--line-2);
background: var(--surface);
color: var(--ink);
font: inherit;
font-weight: 600;
font-size: .82rem;
padding: 9px 15px;
border-radius: var(--r-sm);
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 7px;
transition: border-color .2s, transform .1s, background .2s;
}
.btn:hover { border-color: var(--cyan); background: var(--surface-2); }
.btn:active { transform: translateY(1px); }
:focus-visible {
outline: 2px solid var(--cyan);
outline-offset: 2px;
border-radius: var(--r-sm);
}
/* ---------- KPI cards ---------- */
.kpis {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 20px;
}
.kpi {
position: relative;
background: linear-gradient(180deg, var(--surface-2), var(--surface));
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 18px;
overflow: hidden;
transition: transform .18s, border-color .18s;
}
.kpi::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 3px;
background: linear-gradient(90deg, var(--violet), var(--cyan));
opacity: .8;
}
.kpi:hover { transform: translateY(-3px); border-color: var(--line-2); }
.kpi__top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.kpi__label { font-size: .78rem; color: var(--muted); font-weight: 500; }
.kpi__badge {
font-size: .72rem;
font-weight: 700;
padding: 3px 8px;
border-radius: 999px;
}
.kpi__badge.up { color: var(--pos); background: rgba(52, 211, 153, .12); }
.kpi__badge.down { color: var(--neg); background: rgba(251, 113, 133, .12); }
.kpi__value {
font-size: 1.9rem;
font-weight: 800;
letter-spacing: -0.03em;
margin: 8px 0 10px;
font-variant-numeric: tabular-nums;
}
.kpi__spark svg { width: 100%; height: 30px; display: block; }
.kpi__spark path { opacity: 0; transition: opacity .5s; }
.kpi__spark path.show { opacity: 1; }
/* ---------- Grid ---------- */
.grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 16px;
}
.panel {
background: linear-gradient(180deg, var(--surface-2), var(--surface));
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 20px;
}
.panel--chart { grid-column: 1; grid-row: 1; }
.panel--donut { grid-column: 2; grid-row: 1; }
.panel--table { grid-column: 1 / -1; grid-row: 2; }
.panel__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.panel__head h2 { font-size: 1.02rem; font-weight: 700; letter-spacing: -0.01em; }
.panel__sub { font-size: .78rem; color: var(--muted); margin-top: 3px; }
.chip {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: .76rem;
font-weight: 600;
color: var(--muted);
padding: 5px 10px;
border: 1px solid var(--line);
border-radius: 999px;
}
.chip__swatch {
width: 12px; height: 12px; border-radius: 4px;
background: linear-gradient(180deg, var(--cyan), var(--violet-d));
}
/* ---------- Bar chart ---------- */
.chart { position: relative; }
#barSvg { width: 100%; height: 300px; display: block; overflow: visible; }
.gridline { stroke: var(--line); stroke-width: 1; }
.gridlabel { fill: var(--muted); font-size: 11px; font-family: inherit; }
.bar {
fill: url(#barGrad);
cursor: pointer;
transition: opacity .18s, filter .18s;
}
.bar:hover, .bar:focus-visible { filter: drop-shadow(0 0 10px rgba(34, 211, 238, .55)); }
.bar.dim { opacity: .35; }
.barlabel { fill: var(--muted); font-size: 10.5px; font-family: inherit; text-anchor: middle; }
.chart__tip {
position: absolute;
transform: translate(-50%, -110%);
background: #0a0a0f;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 8px 11px;
font-size: .76rem;
pointer-events: none;
white-space: nowrap;
box-shadow: 0 12px 30px rgba(0, 0, 0, .6);
z-index: 5;
}
.chart__tip strong { display: block; color: var(--white); font-size: .84rem; }
.chart__tip span { color: var(--cyan); font-weight: 700; }
/* ---------- Donut ---------- */
.donut { position: relative; width: 180px; height: 180px; margin: 4px auto 16px; }
#donutSvg { width: 180px; height: 180px; display: block; }
.donut__seg {
fill: none;
stroke-width: 22;
stroke-linecap: round;
cursor: pointer;
transition: stroke-width .18s, opacity .18s;
}
.donut__seg.active { stroke-width: 27; }
.donut__seg.dim { opacity: .3; }
.donut__center {
position: absolute;
inset: 0;
display: grid;
place-content: center;
text-align: center;
pointer-events: none;
}
.donut__center strong { font-size: 1.5rem; font-weight: 800; letter-spacing: -0.02em; }
.donut__center span { font-size: .72rem; color: var(--muted); }
.legend { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.legend li {
display: flex;
align-items: center;
gap: 10px;
font-size: .82rem;
padding: 6px 8px;
border-radius: var(--r-sm);
cursor: pointer;
transition: background .18s;
}
.legend li:hover, .legend li.active { background: var(--surface-2); }
.legend__sw { width: 11px; height: 11px; border-radius: 3px; flex: none; }
.legend__name { flex: 1; color: var(--ink); font-weight: 500; }
.legend__pct { color: var(--muted); font-weight: 700; font-variant-numeric: tabular-nums; }
/* ---------- Table ---------- */
.sort { display: inline-flex; gap: 4px; padding: 3px; background: var(--surface); border: 1px solid var(--line); border-radius: 999px; }
.sort__btn {
border: 0; background: transparent; color: var(--muted);
font: inherit; font-weight: 600; font-size: .78rem;
padding: 6px 12px; border-radius: 999px; cursor: pointer;
transition: color .2s, background .2s;
}
.sort__btn:hover { color: var(--ink); }
.sort__btn.is-active { color: var(--white); background: var(--surface-2); }
.tablewrap { overflow-x: auto; }
.tbl { width: 100%; border-collapse: collapse; min-width: 560px; }
.tbl th {
text-align: left;
font-size: .72rem;
text-transform: uppercase;
letter-spacing: .05em;
color: var(--muted);
font-weight: 600;
padding: 0 14px 10px;
border-bottom: 1px solid var(--line);
}
.tbl th.num, .tbl td.num { text-align: right; }
.tbl td { padding: 13px 14px; border-bottom: 1px solid var(--line); font-size: .88rem; vertical-align: middle; }
.tbl tbody tr { transition: background .16s; }
.tbl tbody tr:hover { background: var(--surface-2); }
.tbl tbody tr:last-child td { border-bottom: 0; }
.rank {
display: inline-grid; place-items: center;
width: 26px; height: 26px; border-radius: 8px;
font-size: .78rem; font-weight: 700;
background: var(--surface); border: 1px solid var(--line);
color: var(--muted);
}
.rank.top { background: linear-gradient(135deg, var(--violet-d), var(--cyan)); color: var(--white); border: 0; }
.ep { display: flex; flex-direction: column; }
.ep__title { font-weight: 600; }
.ep__meta { font-size: .74rem; color: var(--muted); }
.dl { font-weight: 700; font-variant-numeric: tabular-nums; }
.meter { display: flex; align-items: center; gap: 9px; }
.meter__track { width: 84px; height: 7px; border-radius: 999px; background: var(--surface); overflow: hidden; }
.meter__fill { height: 100%; border-radius: 999px; background: linear-gradient(90deg, var(--violet), var(--cyan)); width: 0; transition: width .8s cubic-bezier(.2,.8,.2,1); }
.meter__val { font-size: .78rem; color: var(--muted); font-variant-numeric: tabular-nums; min-width: 34px; }
.trend { font-weight: 700; font-size: .82rem; font-variant-numeric: tabular-nums; display: inline-flex; align-items: center; gap: 4px; }
.trend.up { color: var(--pos); }
.trend.down { color: var(--neg); }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%; bottom: 26px;
transform: translate(-50%, 20px);
background: #0a0a0f;
border: 1px solid var(--line-2);
color: var(--ink);
padding: 11px 18px;
border-radius: 999px;
font-size: .84rem;
font-weight: 600;
box-shadow: 0 16px 40px rgba(0, 0, 0, .55);
opacity: 0;
pointer-events: none;
transition: opacity .28s, transform .28s;
z-index: 50;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.toast::before { content: ""; display: inline-block; width: 8px; height: 8px; margin-right: 8px; border-radius: 50%; background: var(--cyan); vertical-align: middle; }
/* ---------- Responsive ---------- */
@media (max-width: 900px) {
.kpis { grid-template-columns: repeat(2, 1fr); }
.grid { grid-template-columns: 1fr; }
.panel--chart, .panel--donut, .panel--table { grid-column: 1; grid-row: auto; }
}
@media (max-width: 520px) {
.app { padding: 20px 14px 44px; }
.topbar { gap: 12px; }
.topbar__actions { width: 100%; justify-content: space-between; }
.kpis { grid-template-columns: 1fr 1fr; gap: 11px; }
.kpi { padding: 14px; border-radius: var(--r-md); }
.kpi__value { font-size: 1.5rem; }
.panel { padding: 16px; }
#barSvg { height: 240px; }
.brand__meta h1 { font-size: 1.1rem; }
.panel__head { margin-bottom: 12px; }
}
@media (prefers-reduced-motion: reduce) {
* { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}(function () {
"use strict";
/* ---------------- Data model ---------------- */
// Base (30d) episode data. Other ranges are derived with a scale factor.
var EPISODES = [
{ ep: 142, title: "The Attention Economy", guest: "Dr. Lena Ortiz", downloads: 48210, completion: 82, growth: 14.2 },
{ ep: 141, title: "Building in the Open", guest: "Marcus Bell", downloads: 44980, completion: 78, growth: 9.6 },
{ ep: 140, title: "Signals in the Noise", guest: "Priya Nadkarni", downloads: 51740, completion: 85, growth: 21.8 },
{ ep: 139, title: "Quiet Quitting Burnout", guest: "Tomás Reyes", downloads: 39120, completion: 71, growth: -4.3 },
{ ep: 138, title: "The Cost of Free", guest: "Ada Whitfield", downloads: 42600, completion: 76, growth: 6.1 },
{ ep: 137, title: "Designing for Trust", guest: "Kenji Watanabe", downloads: 46810, completion: 80, growth: 11.4 },
{ ep: 136, title: "After the Hype Cycle", guest: "Sofia Marchetti", downloads: 35440, completion: 68, growth: -8.7 },
{ ep: 135, title: "Small Bets, Big Wins", guest: "Idris Bello", downloads: 40950, completion: 74, growth: 5.2 },
{ ep: 134, title: "The Loneliness of Scale", guest: "Hana Kovač", downloads: 37880, completion: 72, growth: 2.9 },
{ ep: 133, title: "What Comes Next", guest: "Elena Frost", downloads: 43370, completion: 77, growth: 8.3 }
];
var PLATFORMS = [
{ name: "Apple Podcasts", pct: 34, color: "#8b5cf6" },
{ name: "Spotify", pct: 41, color: "#22d3ee" },
{ name: "YouTube", pct: 14, color: "#f472b6" },
{ name: "Web / Other", pct: 11, color: "#facc15" }
];
// KPI base values for 30d + per-range multipliers.
var KPIS = {
downloads: { base: 431100, suffix: "", fmt: "int", delta: { 7: 6.4, 30: 12.8, 90: 31.2 } },
subscribers: { base: 58420, suffix: "", fmt: "int", delta: { 7: 2.1, 30: 5.7, 90: 18.4 } },
listen: { base: 76, suffix: "%", fmt: "float1", delta: { 7: 1.2, 30: -0.8, 90: 3.4 } },
minutes: { base: 1284, suffix: "k", fmt: "float1", delta: { 7: 4.9, 30: 9.1, 90: 24.6 } }
};
var RANGE_FACTOR = { 7: 0.28, 30: 1, 90: 2.74 };
var state = { range: 30, sort: "downloads", activePlatform: null };
/* ---------------- Helpers ---------------- */
var $ = function (sel, ctx) { return (ctx || document).querySelector(sel); };
var $$ = function (sel, ctx) { return Array.prototype.slice.call((ctx || document).querySelectorAll(sel)); };
var NS = "http://www.w3.org/2000/svg";
function fmtInt(n) { return Math.round(n).toLocaleString("en-US"); }
function fmtFloat1(n) { return (Math.round(n * 10) / 10).toLocaleString("en-US", { minimumFractionDigits: 1, maximumFractionDigits: 1 }); }
var toastEl = $("#toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2400);
}
function scaledEpisodes() {
var f = RANGE_FACTOR[state.range];
return EPISODES.map(function (e) {
return {
ep: e.ep,
title: e.title,
guest: e.guest,
downloads: Math.round(e.downloads * f),
completion: e.completion,
growth: e.growth * (state.range === 90 ? 1.35 : state.range === 7 ? 0.6 : 1)
};
});
}
/* ---------------- KPI count-up ---------------- */
function countUp(el, target, fmt, suffix) {
var start = 0;
var dur = 900;
var t0 = performance.now();
function frame(now) {
var p = Math.min(1, (now - t0) / dur);
var eased = 1 - Math.pow(1 - p, 3);
var val = start + (target - start) * eased;
el.textContent = (fmt === "int" ? fmtInt(val) : fmtFloat1(val)) + (suffix || "");
if (p < 1) requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
function renderKpis() {
var f = RANGE_FACTOR[state.range];
$$(".kpi").forEach(function (card) {
var key = card.getAttribute("data-metric");
var cfg = KPIS[key];
var valEl = $("[data-value]", card);
var badge = $("[data-delta]", card);
// listen-through is a percentage — does not scale with range window
var raw = key === "listen" ? cfg.base + (cfg.delta[state.range] || 0) : cfg.base * f;
countUp(valEl, raw, cfg.fmt, valEl.getAttribute("data-suffix") || cfg.suffix);
var d = cfg.delta[state.range] || 0;
badge.textContent = (d >= 0 ? "+" : "") + fmtFloat1(d) + "%";
badge.className = "kpi__badge " + (d >= 0 ? "up" : "down");
// sparkline
var path = $("[data-spark]", card);
var pts = sparkPath(key, state.range);
path.setAttribute("d", pts);
path.classList.remove("show");
requestAnimationFrame(function () { path.classList.add("show"); });
});
}
function sparkPath(key, range) {
// deterministic pseudo-random wiggle per key/range
var seed = key.length * 7 + range;
var pts = [];
for (var i = 0; i < 12; i++) {
var s = Math.sin(seed + i * 1.3) * 0.5 + 0.5;
var trend = i / 11 * 0.5;
var y = 30 - (s * 0.5 + trend) * 26;
pts.push([i / 11 * 120, y]);
}
return pts.map(function (p, i) { return (i === 0 ? "M" : "L") + p[0].toFixed(1) + " " + p[1].toFixed(1); }).join(" ");
}
/* ---------------- Bar chart ---------------- */
var svg = $("#barSvg");
var barsG = $("#bars");
var gridG = $("#gridLines");
var tip = $("#tip");
var VW = 640, VH = 300, PAD_L = 8, PAD_R = 8, PAD_B = 34, PAD_T = 12;
function renderChart() {
var data = scaledEpisodes().slice().sort(function (a, b) { return a.ep - b.ep; });
var max = Math.max.apply(null, data.map(function (d) { return d.downloads; }));
var niceMax = Math.ceil(max / 10000) * 10000;
barsG.innerHTML = "";
gridG.innerHTML = "";
var plotW = VW - PAD_L - PAD_R;
var plotH = VH - PAD_T - PAD_B;
// gridlines + y labels
for (var g = 0; g <= 4; g++) {
var yv = niceMax * g / 4;
var y = PAD_T + plotH - (yv / niceMax) * plotH;
var line = document.createElementNS(NS, "line");
line.setAttribute("class", "gridline");
line.setAttribute("x1", PAD_L); line.setAttribute("x2", VW - PAD_R);
line.setAttribute("y1", y); line.setAttribute("y2", y);
gridG.appendChild(line);
var lbl = document.createElementNS(NS, "text");
lbl.setAttribute("class", "gridlabel");
lbl.setAttribute("x", PAD_L + 2); lbl.setAttribute("y", y - 4);
lbl.textContent = yv >= 1000 ? Math.round(yv / 1000) + "k" : Math.round(yv);
gridG.appendChild(lbl);
}
var n = data.length;
var slot = plotW / n;
var bw = Math.min(38, slot * 0.6);
data.forEach(function (d, i) {
var h = (d.downloads / niceMax) * plotH;
var x = PAD_L + slot * i + (slot - bw) / 2;
var yTop = PAD_T + plotH - h;
var rect = document.createElementNS(NS, "rect");
rect.setAttribute("class", "bar");
rect.setAttribute("x", x);
rect.setAttribute("width", bw);
rect.setAttribute("rx", 5);
rect.setAttribute("tabindex", "0");
rect.setAttribute("role", "listitem");
rect.setAttribute("aria-label", "Episode " + d.ep + ", " + d.title + ", " + fmtInt(d.downloads) + " downloads");
// start collapsed at baseline
rect.setAttribute("y", PAD_T + plotH);
rect.setAttribute("height", 0);
barsG.appendChild(rect);
// animate
(function (r, ty, th, idx) {
setTimeout(function () {
r.style.transition = "y .7s cubic-bezier(.2,.8,.2,1), height .7s cubic-bezier(.2,.8,.2,1)";
r.setAttribute("y", ty);
r.setAttribute("height", th);
}, 40 + idx * 45);
})(rect, yTop, h, i);
// x label (episode number)
var xl = document.createElementNS(NS, "text");
xl.setAttribute("class", "barlabel");
xl.setAttribute("x", x + bw / 2);
xl.setAttribute("y", VH - 12);
xl.textContent = "#" + d.ep;
barsG.appendChild(xl);
// interactions
function show() { showTip(rect, d); }
function hide() { tip.hidden = true; }
rect.addEventListener("mouseenter", show);
rect.addEventListener("mousemove", show);
rect.addEventListener("mouseleave", hide);
rect.addEventListener("focus", show);
rect.addEventListener("blur", hide);
rect.addEventListener("click", function () { toast("Ep " + d.ep + " · " + d.title + " — " + fmtInt(d.downloads) + " downloads"); });
rect.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); rect.click(); }
});
});
}
function showTip(rect, d) {
var wrap = $("#chart");
var rb = rect.getBoundingClientRect();
var wb = wrap.getBoundingClientRect();
tip.innerHTML = "<strong>Ep " + d.ep + " · " + d.title + "</strong><span>" + fmtInt(d.downloads) + " downloads</span> · " + d.completion + "% listened";
tip.hidden = false;
tip.style.left = (rb.left - wb.left + rb.width / 2) + "px";
tip.style.top = (rb.top - wb.top) + "px";
}
/* ---------------- Donut ---------------- */
var donutG = $("#donutG");
var R = 70, C = 2 * Math.PI * R;
function renderDonut() {
donutG.innerHTML = "";
var offset = 0;
var segs = [];
PLATFORMS.forEach(function (p, i) {
var len = p.pct / 100 * C;
var circ = document.createElementNS(NS, "circle");
circ.setAttribute("class", "donut__seg");
circ.setAttribute("r", R);
circ.setAttribute("cx", 0);
circ.setAttribute("cy", 0);
circ.setAttribute("stroke", p.color);
circ.setAttribute("transform", "rotate(-90)");
circ.setAttribute("stroke-dasharray", "0 " + C);
circ.setAttribute("stroke-dashoffset", -offset);
circ.setAttribute("tabindex", "0");
circ.setAttribute("role", "button");
circ.setAttribute("aria-label", p.name + " " + p.pct + " percent");
donutG.appendChild(circ);
(function (c, l, idx) {
setTimeout(function () {
c.style.transition = "stroke-dasharray .8s ease, stroke-width .18s, opacity .18s";
c.setAttribute("stroke-dasharray", l + " " + (C - l));
}, 120 + idx * 120);
})(circ, len, i);
segs.push(circ);
circ.addEventListener("mouseenter", function () { highlight(i); });
circ.addEventListener("focus", function () { highlight(i); });
circ.addEventListener("mouseleave", function () { highlight(null); });
circ.addEventListener("blur", function () { highlight(null); });
offset += len;
});
// legend
var legend = $("#legend");
legend.innerHTML = "";
PLATFORMS.forEach(function (p, i) {
var li = document.createElement("li");
li.setAttribute("tabindex", "0");
li.innerHTML = '<span class="legend__sw" style="background:' + p.color + '"></span>' +
'<span class="legend__name">' + p.name + '</span>' +
'<span class="legend__pct">' + p.pct + '%</span>';
li.addEventListener("mouseenter", function () { highlight(i); });
li.addEventListener("mouseleave", function () { highlight(null); });
li.addEventListener("focus", function () { highlight(i); });
li.addEventListener("blur", function () { highlight(null); });
legend.appendChild(li);
});
window._donutSegs = segs;
}
function highlight(idx) {
var segs = window._donutSegs || [];
var items = $$("#legend li");
var center = $("#donutValue");
var label = $("#donutLabel");
segs.forEach(function (s, i) {
s.classList.toggle("active", idx === i);
s.classList.toggle("dim", idx !== null && idx !== i);
});
items.forEach(function (li, i) { li.classList.toggle("active", idx === i); });
if (idx === null) {
center.textContent = "100%";
label.textContent = "All platforms";
} else {
center.textContent = PLATFORMS[idx].pct + "%";
label.textContent = PLATFORMS[idx].name;
}
}
/* ---------------- Table ---------------- */
function renderTable() {
var data = scaledEpisodes();
data.sort(function (a, b) {
if (state.sort === "downloads") return b.downloads - a.downloads;
if (state.sort === "completion") return b.completion - a.completion;
return b.growth - a.growth;
});
var tbody = $("#tbody");
tbody.innerHTML = "";
data.forEach(function (d, i) {
var tr = document.createElement("tr");
var up = d.growth >= 0;
tr.innerHTML =
'<td><span class="rank ' + (i < 3 ? "top" : "") + '">' + (i + 1) + '</span></td>' +
'<td><div class="ep"><span class="ep__title">' + d.title + '</span>' +
'<span class="ep__meta">Ep ' + d.ep + ' · ' + d.guest + '</span></div></td>' +
'<td class="num"><span class="dl">' + fmtInt(d.downloads) + '</span></td>' +
'<td><div class="meter"><div class="meter__track"><div class="meter__fill" data-w="' + d.completion + '"></div></div>' +
'<span class="meter__val">' + d.completion + '%</span></div></td>' +
'<td class="num"><span class="trend ' + (up ? "up" : "down") + '">' +
(up ? "▲" : "▼") + " " + fmtFloat1(Math.abs(d.growth)) + '%</span></td>';
tbody.appendChild(tr);
});
// animate meters
requestAnimationFrame(function () {
$$(".meter__fill", tbody).forEach(function (m) { m.style.width = m.getAttribute("data-w") + "%"; });
});
}
/* ---------------- Controls ---------------- */
$$(".range__btn").forEach(function (btn) {
btn.addEventListener("click", function () {
if (+btn.getAttribute("data-range") === state.range) return;
state.range = +btn.getAttribute("data-range");
$$(".range__btn").forEach(function (b) {
var on = b === btn;
b.classList.toggle("is-active", on);
b.setAttribute("aria-selected", on ? "true" : "false");
});
$("#chartSub").textContent = "Last " + state.range + " days · latest 10 episodes";
renderKpis();
renderChart();
renderTable();
toast("Range updated to " + state.range + " days");
});
});
$$(".sort__btn").forEach(function (btn) {
btn.addEventListener("click", function () {
state.sort = btn.getAttribute("data-sort");
$$(".sort__btn").forEach(function (b) { b.classList.toggle("is-active", b === btn); });
renderTable();
toast("Sorted by " + btn.textContent.trim().toLowerCase());
});
});
$("#exportBtn").addEventListener("click", function () {
toast("Report for last " + state.range + " days exported (CSV)");
});
/* ---------------- Init ---------------- */
renderKpis();
renderChart();
renderDonut();
renderTable();
window.addEventListener("resize", function () {
clearTimeout(window._rz);
window._rz = setTimeout(function () { tip.hidden = true; }, 100);
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Signal & Noise — Stats Dashboard</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="app">
<!-- ============ HEADER ============ -->
<header class="topbar">
<div class="brand">
<div class="brand__logo" aria-hidden="true">
<span class="wave"><i></i><i></i><i></i><i></i><i></i></span>
</div>
<div class="brand__meta">
<h1>Signal & Noise</h1>
<p><span class="dot" aria-hidden="true"></span> Live analytics · synced 2 min ago</p>
</div>
</div>
<div class="topbar__actions">
<div class="range" role="tablist" aria-label="Time range">
<button role="tab" class="range__btn" data-range="7" aria-selected="false">7d</button>
<button role="tab" class="range__btn is-active" data-range="30" aria-selected="true">30d</button>
<button role="tab" class="range__btn" data-range="90" aria-selected="false">90d</button>
</div>
<button class="btn btn--ghost" id="exportBtn" type="button">
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M12 3v12m0 0l-4-4m4 4l4-4M5 21h14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
Export
</button>
</div>
</header>
<!-- ============ KPI CARDS ============ -->
<section class="kpis" aria-label="Key metrics">
<article class="kpi" data-metric="downloads">
<div class="kpi__top">
<span class="kpi__label">Total downloads</span>
<span class="kpi__badge" data-delta></span>
</div>
<div class="kpi__value" data-value>0</div>
<div class="kpi__spark" aria-hidden="true"><svg viewBox="0 0 120 32" preserveAspectRatio="none"><path data-spark d="" fill="none" stroke="url(#gradA)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/><defs><linearGradient id="gradA" x1="0" x2="1"><stop offset="0" stop-color="#8b5cf6"/><stop offset="1" stop-color="#22d3ee"/></linearGradient></defs></svg></div>
</article>
<article class="kpi" data-metric="subscribers">
<div class="kpi__top">
<span class="kpi__label">Subscribers</span>
<span class="kpi__badge" data-delta></span>
</div>
<div class="kpi__value" data-value>0</div>
<div class="kpi__spark" aria-hidden="true"><svg viewBox="0 0 120 32" preserveAspectRatio="none"><path data-spark d="" fill="none" stroke="url(#gradB)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/><defs><linearGradient id="gradB" x1="0" x2="1"><stop offset="0" stop-color="#22d3ee"/><stop offset="1" stop-color="#f472b6"/></linearGradient></defs></svg></div>
</article>
<article class="kpi" data-metric="listen">
<div class="kpi__top">
<span class="kpi__label">Avg. listen-through</span>
<span class="kpi__badge" data-delta></span>
</div>
<div class="kpi__value" data-value data-suffix="%">0</div>
<div class="kpi__spark" aria-hidden="true"><svg viewBox="0 0 120 32" preserveAspectRatio="none"><path data-spark d="" fill="none" stroke="url(#gradC)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/><defs><linearGradient id="gradC" x1="0" x2="1"><stop offset="0" stop-color="#f472b6"/><stop offset="1" stop-color="#8b5cf6"/></linearGradient></defs></svg></div>
</article>
<article class="kpi" data-metric="minutes">
<div class="kpi__top">
<span class="kpi__label">Minutes streamed</span>
<span class="kpi__badge" data-delta></span>
</div>
<div class="kpi__value" data-value data-suffix="k">0</div>
<div class="kpi__spark" aria-hidden="true"><svg viewBox="0 0 120 32" preserveAspectRatio="none"><path data-spark d="" fill="none" stroke="url(#gradD)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/><defs><linearGradient id="gradD" x1="0" x2="1"><stop offset="0" stop-color="#8b5cf6"/><stop offset="1" stop-color="#22d3ee"/></linearGradient></defs></svg></div>
</article>
</section>
<!-- ============ MAIN GRID ============ -->
<div class="grid">
<!-- Bar chart -->
<section class="panel panel--chart" aria-label="Downloads per episode">
<div class="panel__head">
<div>
<h2>Downloads per episode</h2>
<p class="panel__sub" id="chartSub">Last 30 days · latest 10 episodes</p>
</div>
<span class="chip"><span class="chip__swatch"></span> Downloads</span>
</div>
<div class="chart" id="chart">
<svg id="barSvg" viewBox="0 0 640 300" role="img" aria-label="Bar chart of downloads per episode" preserveAspectRatio="none">
<defs>
<linearGradient id="barGrad" x1="0" y1="1" x2="0" y2="0">
<stop offset="0" stop-color="#7c3aed"/>
<stop offset="1" stop-color="#22d3ee"/>
</linearGradient>
</defs>
<g id="gridLines"></g>
<g id="bars"></g>
</svg>
<div class="chart__tip" id="tip" role="status" aria-live="polite" hidden></div>
</div>
</section>
<!-- Donut -->
<section class="panel panel--donut" aria-label="Platform breakdown">
<div class="panel__head">
<div>
<h2>Listeners by platform</h2>
<p class="panel__sub">Share of total plays</p>
</div>
</div>
<div class="donut">
<svg viewBox="0 0 180 180" id="donutSvg" aria-hidden="true">
<g transform="translate(90,90)" id="donutG"></g>
</svg>
<div class="donut__center">
<strong id="donutValue">100%</strong>
<span id="donutLabel">All platforms</span>
</div>
</div>
<ul class="legend" id="legend"></ul>
</section>
<!-- Top episodes table -->
<section class="panel panel--table" aria-label="Top episodes">
<div class="panel__head">
<div>
<h2>Top episodes</h2>
<p class="panel__sub">Ranked for the selected range</p>
</div>
<div class="sort" role="group" aria-label="Sort episodes">
<button class="sort__btn is-active" data-sort="downloads" type="button">Downloads</button>
<button class="sort__btn" data-sort="completion" type="button">Listen-through</button>
<button class="sort__btn" data-sort="growth" type="button">Growth</button>
</div>
</div>
<div class="tablewrap">
<table class="tbl">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Episode</th>
<th scope="col" class="num">Downloads</th>
<th scope="col">Listen-through</th>
<th scope="col" class="num">Growth</th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
</div>
</section>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A creator analytics dashboard for a fictional podcast, Signal & Noise, built in the podcast design language: near-black surfaces, glowing violet-to-cyan accents and vibrant waveform gradients. The header carries the show identity, a live “synced” indicator and a range switcher (7d / 30d / 90d) that recomputes every figure on the page. Four KPI cards animate their numbers with a count-up effect and show a coloured delta badge versus the previous period.
The centrepiece is an inline-SVG bar chart of downloads per episode that animates its bars from the baseline on load and on every range change. Each bar exposes a hover/focus tooltip with the exact episode title and download count, and the whole chart is keyboard navigable. Below it, a sortable top-episodes table ranks episodes by downloads, listen-through or growth, with trend badges and mini completion meters, while a hand-drawn SVG donut breaks listeners down by platform with an interactive legend.
Everything is vanilla: no frameworks, no build step, no external chart libraries. Interactions include range switching, table sorting, donut segment highlighting, animated meters and a small toast() helper for feedback — all keyboard-usable with visible focus states and AA-contrast text on the dark theme.