Coworking — Occupancy Dashboard
A warm industrial occupancy and utilization dashboard for a fictional coworking studio. It surfaces live KPIs for occupancy, desk utilization, peak hours and checked-in members, an interactive occupancy-over-day line chart with hover tooltips, a zone-by-hour heatmap with drill-in detail, and a list of underused resources flagged for review. A timeframe toggle reflows every number, the curve and the heatmap between today, week and month views.
MCP
Code
:root {
--concrete: #efeae3;
--concrete-d: #e2dcd2;
--amber: #e8902b;
--amber-d: #cc7918;
--amber-50: #fdf1e2;
--char: #1c1b19;
--ink: #26241f;
--ink-2: #4a463e;
--muted: #7b766c;
--bg: #f6f3ee;
--surface: #ffffff;
--plant: #5f7a52;
--line: rgba(28, 27, 25, 0.1);
--line-2: rgba(28, 27, 25, 0.18);
--ok: #2f9e6f;
--warn: #d98a2b;
--danger: #d4503e;
--occupied: #d4503e;
--free: #2f9e6f;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-sm: 0 1px 2px rgba(28, 27, 25, 0.06);
--sh-md: 0 6px 22px rgba(28, 27, 25, 0.08);
--sh-lg: 0 18px 50px rgba(28, 27, 25, 0.14);
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
}
body {
font-family: "Inter", system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3 { margin: 0; letter-spacing: -0.02em; }
p { margin: 0; }
button { font-family: inherit; cursor: pointer; }
.app {
display: grid;
grid-template-columns: 248px 1fr;
min-height: 100vh;
}
/* ---------- Sidebar ---------- */
.sidebar {
background: var(--char);
color: var(--concrete);
padding: 22px 18px;
display: flex;
flex-direction: column;
gap: 26px;
position: sticky;
top: 0;
height: 100vh;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
font-weight: 800;
font-size: 18px;
letter-spacing: -0.02em;
}
.brand-mark {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: var(--r-sm);
background: var(--amber);
color: var(--char);
font-size: 16px;
}
.nav { display: flex; flex-direction: column; gap: 4px; }
.nav-item {
display: flex;
align-items: center;
gap: 11px;
padding: 10px 12px;
border-radius: var(--r-sm);
color: rgba(239, 234, 227, 0.66);
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: background 0.16s, color 0.16s;
}
.nav-item .ni-ico { font-size: 14px; width: 18px; text-align: center; opacity: 0.9; }
.nav-item:hover { background: rgba(255, 255, 255, 0.06); color: var(--concrete); }
.nav-item.is-active {
background: rgba(232, 144, 43, 0.16);
color: #fff;
font-weight: 600;
}
.sidebar-foot {
margin-top: auto;
display: flex;
align-items: center;
gap: 9px;
font-size: 12px;
color: rgba(239, 234, 227, 0.55);
}
.live-dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--ok);
box-shadow: 0 0 0 0 rgba(47, 158, 111, 0.6);
animation: pulse 2.2s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(47, 158, 111, 0.5); }
70% { box-shadow: 0 0 0 7px rgba(47, 158, 111, 0); }
100% { box-shadow: 0 0 0 0 rgba(47, 158, 111, 0); }
}
/* ---------- Main ---------- */
.main { padding: 26px 32px 48px; max-width: 1240px; }
.topbar {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
margin-bottom: 22px;
}
.eyebrow {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--amber-d);
margin-bottom: 4px;
}
.topbar h1 { font-size: 27px; font-weight: 800; color: var(--char); }
.topbar-actions { display: flex; align-items: center; gap: 14px; }
.seg {
display: inline-flex;
background: var(--concrete);
border: 1px solid var(--line);
border-radius: 999px;
padding: 3px;
}
.seg-btn {
border: 0;
background: transparent;
padding: 7px 16px;
border-radius: 999px;
font-size: 13px;
font-weight: 600;
color: var(--ink-2);
transition: background 0.16s, color 0.16s, box-shadow 0.16s;
}
.seg-btn:hover { color: var(--char); }
.seg-btn.is-active {
background: var(--surface);
color: var(--char);
box-shadow: var(--sh-sm);
}
.seg-btn:focus-visible { outline: 2px solid var(--amber); outline-offset: 2px; }
.avatar {
width: 38px; height: 38px;
border-radius: 50%;
background: linear-gradient(135deg, var(--amber), var(--amber-d));
color: #fff;
display: grid;
place-items: center;
font-size: 13px;
font-weight: 700;
box-shadow: var(--sh-sm);
}
/* ---------- KPIs ---------- */
.kpis {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 18px;
}
.kpi {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 16px 18px;
box-shadow: var(--sh-sm);
transition: transform 0.16s, box-shadow 0.16s;
}
.kpi:hover { transform: translateY(-2px); box-shadow: var(--sh-md); }
.kpi-top { display: flex; align-items: center; justify-content: space-between; }
.kpi-label {
font-size: 12.5px;
font-weight: 600;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.trend {
font-size: 11.5px;
font-weight: 700;
padding: 2px 7px;
border-radius: 999px;
}
.trend.up { color: var(--ok); background: rgba(47, 158, 111, 0.12); }
.trend.down { color: var(--danger); background: rgba(212, 80, 62, 0.12); }
.trend.flat { color: var(--muted); background: rgba(123, 118, 108, 0.12); }
.kpi-value {
font-size: 34px;
font-weight: 800;
color: var(--char);
letter-spacing: -0.03em;
margin: 8px 0 10px;
line-height: 1;
}
.kpi-value .unit { font-size: 18px; color: var(--muted); margin-left: 2px; font-weight: 700; }
.bar {
height: 7px;
border-radius: 999px;
background: var(--concrete-d);
overflow: hidden;
}
.bar-fill {
display: block;
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, var(--amber), var(--amber-d));
transition: width 0.6s cubic-bezier(0.3, 0.8, 0.3, 1);
}
.bar-fill.plant { background: linear-gradient(90deg, var(--plant), #4d6643); }
.kpi-sub { font-size: 12px; color: var(--muted); margin-top: 9px; }
.spark { display: flex; align-items: flex-end; gap: 3px; height: 24px; margin: 6px 0 0; }
.spark span {
flex: 1;
background: var(--amber-50);
border-radius: 2px 2px 0 0;
min-height: 3px;
}
.spark span.hot { background: var(--amber); }
.avatars { display: flex; margin: 6px 0 0; }
.avatars span {
width: 26px; height: 26px;
border-radius: 50%;
margin-left: -8px;
border: 2px solid var(--surface);
display: grid;
place-items: center;
font-size: 10px;
font-weight: 700;
color: #fff;
}
.avatars span:first-child { margin-left: 0; }
/* ---------- Grid ---------- */
.grid {
display: grid;
grid-template-columns: 1.65fr 1fr;
gap: 16px;
margin-bottom: 18px;
}
.panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 20px 22px;
box-shadow: var(--sh-sm);
}
.panel-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
}
.panel-head h2 { font-size: 16px; font-weight: 700; color: var(--char); }
.panel-sub { font-size: 12.5px; color: var(--muted); margin-top: 3px; }
.legend { display: flex; gap: 14px; align-items: center; }
.lg { font-size: 12px; color: var(--ink-2); display: inline-flex; align-items: center; gap: 6px; }
.dot { width: 10px; height: 10px; border-radius: 3px; display: inline-block; }
.dot.amber { background: var(--amber); }
.dot.line { background: var(--line-2); }
.pill {
font-size: 11px;
font-weight: 700;
padding: 3px 10px;
border-radius: 999px;
}
.pill.warn { background: rgba(217, 138, 43, 0.14); color: var(--amber-d); }
/* ---------- Chart ---------- */
.chart { position: relative; }
.chart svg { width: 100%; height: 240px; display: block; }
.chart-x {
display: flex;
justify-content: space-between;
font-size: 11px;
color: var(--muted);
margin-top: 6px;
}
.chart-dot { cursor: pointer; transition: r 0.12s; }
.chart-tip {
position: absolute;
transform: translate(-50%, -100%);
background: var(--char);
color: #fff;
padding: 8px 11px;
border-radius: var(--r-sm);
font-size: 12px;
font-weight: 500;
pointer-events: none;
white-space: nowrap;
box-shadow: var(--sh-md);
z-index: 5;
}
.chart-tip b { color: var(--amber); }
/* ---------- Underused ---------- */
.under-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.under-item {
display: flex;
align-items: center;
gap: 12px;
padding: 11px 12px;
border: 1px solid var(--line);
border-radius: var(--r-md);
background: var(--bg);
transition: border-color 0.16s, background 0.16s;
}
.under-item:hover { border-color: var(--line-2); background: var(--surface); }
.ui-ico {
width: 36px; height: 36px;
border-radius: var(--r-sm);
display: grid; place-items: center;
font-size: 16px;
flex-shrink: 0;
background: var(--amber-50);
color: var(--amber-d);
}
.ui-body { flex: 1; min-width: 0; }
.ui-name { font-size: 13.5px; font-weight: 600; color: var(--ink); }
.ui-meta { font-size: 11.5px; color: var(--muted); }
.ui-use {
text-align: right;
flex-shrink: 0;
}
.ui-pct { font-size: 15px; font-weight: 800; color: var(--danger); letter-spacing: -0.02em; }
.ui-pct.warn { color: var(--warn); }
.ui-track { width: 56px; height: 5px; border-radius: 999px; background: var(--concrete-d); margin-top: 4px; overflow: hidden; }
.ui-track i { display: block; height: 100%; border-radius: 999px; background: var(--danger); }
/* ---------- Heatmap ---------- */
.heat-panel { position: relative; }
.heat-legend .scale {
width: 110px; height: 9px; border-radius: 999px;
background: linear-gradient(90deg, #f5efe6, #f6cf9a, var(--amber), var(--danger));
display: inline-block;
}
.heat-wrap { overflow-x: auto; padding-bottom: 4px; }
.heat {
display: grid;
gap: 5px;
min-width: 640px;
}
.heat-row { display: contents; }
.heat-label {
font-size: 12px;
font-weight: 600;
color: var(--ink-2);
display: flex;
align-items: center;
padding-right: 6px;
white-space: nowrap;
}
.heat-label.zone {
cursor: pointer;
border-radius: var(--r-sm);
}
.heat-label.zone:hover { color: var(--amber-d); }
.heat-label.zone:focus-visible { outline: 2px solid var(--amber); outline-offset: 2px; }
.heat-head {
font-size: 11px;
color: var(--muted);
text-align: center;
font-weight: 600;
}
.cell {
height: 30px;
border-radius: 6px;
cursor: pointer;
transition: transform 0.12s, box-shadow 0.12s;
position: relative;
}
.cell:hover {
transform: scale(1.12);
box-shadow: var(--sh-md);
z-index: 3;
outline: 2px solid var(--char);
}
.cell:focus-visible { outline: 2px solid var(--char); }
/* ---------- Drill ---------- */
.drill {
margin-top: 16px;
position: relative;
border: 1px solid var(--line);
border-left: 4px solid var(--amber);
border-radius: var(--r-md);
background: var(--amber-50);
padding: 16px 18px;
}
.drill h3 { font-size: 15px; color: var(--char); margin-bottom: 12px; }
.drill-close {
position: absolute;
top: 12px; right: 12px;
border: 0; background: transparent;
font-size: 14px; color: var(--muted);
width: 26px; height: 26px; border-radius: 50%;
}
.drill-close:hover { background: rgba(28, 27, 25, 0.08); color: var(--char); }
.drill-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
}
.ds-item .ds-val { font-size: 22px; font-weight: 800; color: var(--char); letter-spacing: -0.02em; }
.ds-item .ds-lab { font-size: 11.5px; color: var(--ink-2); margin-top: 2px; }
/* ---------- Toast ---------- */
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translate(-50%, 18px);
background: var(--char);
color: #fff;
padding: 11px 18px;
border-radius: 999px;
font-size: 13px;
font-weight: 500;
box-shadow: var(--sh-lg);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s, transform 0.22s;
z-index: 40;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- Responsive ---------- */
@media (max-width: 960px) {
.app { grid-template-columns: 1fr; }
.sidebar {
position: static; height: auto; flex-direction: row;
align-items: center; gap: 16px; flex-wrap: wrap;
}
.nav { flex-direction: row; flex-wrap: wrap; gap: 4px; }
.sidebar-foot { margin: 0 0 0 auto; }
.kpis { grid-template-columns: repeat(2, 1fr); }
.grid { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
.main { padding: 18px 16px 40px; }
.topbar { flex-direction: column; align-items: flex-start; }
.topbar h1 { font-size: 22px; }
.kpis { grid-template-columns: 1fr; }
.kpi-value { font-size: 30px; }
.drill-stats { grid-template-columns: repeat(2, 1fr); }
.nav-item span:not(.ni-ico) { display: none; }
.nav-item { padding: 9px 11px; }
}(function () {
"use strict";
/* ---------- Toast helper ---------- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2400);
}
var CAPACITY = 156;
var HOURS = ["07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20"];
var ZONES = [
{ id: "open", name: "Open Loft Desks", ico: "▦", seats: 64 },
{ id: "quiet", name: "Quiet Studio", ico: "✎", seats: 28 },
{ id: "phone", name: "Phone Booths", ico: "☏", seats: 8 },
{ id: "lounge", name: "Garden Lounge", ico: "❧", seats: 24 },
{ id: "meet", name: "Meeting Rooms", ico: "◷", seats: 20 },
{ id: "lab", name: "Maker Lab", ico: "⚒", seats: 12 }
];
/* Deterministic pseudo-random so data feels real and stable per timeframe. */
function seeded(seed) {
return function () {
seed = (seed * 9301 + 49297) % 233280;
return seed / 233280;
};
}
/* Per-timeframe KPI + curve config. */
var RANGES = {
today: {
occ: 72, util: 64, peak: "11:00", peakPct: 88, members: 128, memTotal: 214,
trendOcc: ["up", "▲ 6%"], trendUtil: ["up", "▲ 3%"], trendPeak: ["flat", "— steady"], trendMem: ["up", "▲ 12"],
sub: "Live seats in use · 07:00–20:00", seed: 41, amp: 1
},
week: {
occ: 68, util: 61, peak: "11:30", peakPct: 84, members: 187, memTotal: 214,
trendOcc: ["up", "▲ 2%"], trendUtil: ["down", "▼ 1%"], trendPeak: ["flat", "— steady"], trendMem: ["up", "▲ 9"],
sub: "Daily average · last 7 days", seed: 77, amp: 0.94
},
month: {
occ: 65, util: 58, peak: "11:00", peakPct: 81, members: 203, memTotal: 214,
trendOcc: ["down", "▼ 3%"], trendUtil: ["down", "▼ 2%"], trendPeak: ["flat", "— steady"], trendMem: ["up", "▲ 4"],
sub: "Daily average · last 30 days", seed: 113, amp: 0.88
}
};
/* Build an occupancy curve (rising morning, midday peak, afternoon dip, evening fall). */
function buildCurve(cfg) {
var rnd = seeded(cfg.seed);
var shape = [0.34, 0.52, 0.72, 0.86, 1.0, 0.78, 0.7, 0.82, 0.9, 0.74, 0.58, 0.4, 0.26, 0.16];
var peakSeats = Math.round(CAPACITY * (cfg.peakPct / 100));
return shape.map(function (s, i) {
var noise = (rnd() - 0.5) * 0.06;
var v = Math.max(0.08, Math.min(1, (s + noise) * cfg.amp));
return Math.round(v * peakSeats);
});
}
/* ---------- Animate number ---------- */
function animateNum(el, to, suffix) {
suffix = suffix || "";
var from = parseInt(el.textContent, 10);
if (isNaN(from)) { el.textContent = to + suffix; return; }
var start = performance.now();
var dur = 520;
function step(now) {
var t = Math.min(1, (now - start) / dur);
var eased = 1 - Math.pow(1 - t, 3);
el.textContent = Math.round(from + (to - from) * eased) + suffix;
if (t < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
/* ---------- KPIs ---------- */
function setTrend(el, data) {
el.className = "trend " + data[0];
el.textContent = data[1];
}
function renderKpis(cfg) {
animateNum(document.getElementById("kpiOcc"), cfg.occ);
animateNum(document.getElementById("kpiUtil"), cfg.util);
animateNum(document.getElementById("kpiMem"), cfg.members);
document.getElementById("kpiPeak").textContent = cfg.peak;
document.getElementById("kpiOccBar").style.width = cfg.occ + "%";
document.getElementById("kpiUtilBar").style.width = cfg.util + "%";
setTrend(document.getElementById("trendOcc"), cfg.trendOcc);
setTrend(document.getElementById("trendUtil"), cfg.trendUtil);
setTrend(document.getElementById("trendPeak"), cfg.trendPeak);
setTrend(document.getElementById("trendMem"), cfg.trendMem);
var inUse = Math.round(CAPACITY * cfg.occ / 100);
document.querySelector(".kpi .kpi-sub").textContent = inUse + " of " + CAPACITY + " seats in use";
document.querySelectorAll(".kpi .kpi-sub")[3].textContent = "of " + cfg.memTotal + " active members";
}
/* Spark + avatars (static-ish decorative) */
function renderSpark(cfg) {
var curve = buildCurve(cfg);
var max = Math.max.apply(null, curve);
var sp = document.getElementById("kpiSpark");
sp.innerHTML = "";
curve.forEach(function (v) {
var b = document.createElement("span");
b.style.height = Math.max(10, (v / max) * 100) + "%";
if (v === max) b.className = "hot";
sp.appendChild(b);
});
}
function renderAvatars() {
var names = ["Aria N.", "Tomás R.", "Lin W.", "Kofi A.", "Sofia M."];
var hues = ["#e8902b", "#5f7a52", "#cc7918", "#4a463e", "#2f9e6f"];
var wrap = document.getElementById("kpiAvatars");
wrap.innerHTML = "";
names.forEach(function (n, i) {
var s = document.createElement("span");
s.style.background = hues[i];
s.textContent = n.split(" ").map(function (x) { return x[0]; }).join("");
s.title = n;
wrap.appendChild(s);
});
var more = document.createElement("span");
more.style.background = "#9a9488";
more.textContent = "+";
wrap.appendChild(more);
}
/* ---------- Line chart ---------- */
var svg = document.getElementById("chartSvg");
var chartTip = document.getElementById("chartTip");
var SVGNS = "http://www.w3.org/2000/svg";
var W = 720, H = 240, PADX = 14, PADY = 18;
function renderChart(cfg) {
var data = buildCurve(cfg);
svg.innerHTML = "";
var maxY = CAPACITY;
var innerW = W - PADX * 2;
var innerH = H - PADY * 2;
function x(i) { return PADX + (i / (data.length - 1)) * innerW; }
function y(v) { return PADY + innerH - (v / maxY) * innerH; }
// grid lines + capacity line
[0.25, 0.5, 0.75, 1].forEach(function (g) {
var gl = document.createElementNS(SVGNS, "line");
gl.setAttribute("x1", PADX); gl.setAttribute("x2", W - PADX);
gl.setAttribute("y1", y(maxY * g)); gl.setAttribute("y2", y(maxY * g));
gl.setAttribute("stroke", g === 1 ? "rgba(28,27,25,0.22)" : "rgba(28,27,25,0.07)");
gl.setAttribute("stroke-dasharray", g === 1 ? "5 4" : "0");
svg.appendChild(gl);
});
// area + line path
var lineD = "", areaD = "M " + x(0) + " " + y(0);
data.forEach(function (v, i) {
var px = x(i), py = y(v);
lineD += (i === 0 ? "M " : "L ") + px + " " + py + " ";
areaD += " L " + px + " " + py;
});
areaD += " L " + x(data.length - 1) + " " + y(0) + " Z";
var grad = document.createElementNS(SVGNS, "linearGradient");
grad.setAttribute("id", "areaGrad");
grad.setAttribute("x1", "0"); grad.setAttribute("y1", "0");
grad.setAttribute("x2", "0"); grad.setAttribute("y2", "1");
grad.innerHTML =
'<stop offset="0%" stop-color="#e8902b" stop-opacity="0.28"/>' +
'<stop offset="100%" stop-color="#e8902b" stop-opacity="0"/>';
svg.appendChild(grad);
var area = document.createElementNS(SVGNS, "path");
area.setAttribute("d", areaD);
area.setAttribute("fill", "url(#areaGrad)");
svg.appendChild(area);
var line = document.createElementNS(SVGNS, "path");
line.setAttribute("d", lineD);
line.setAttribute("fill", "none");
line.setAttribute("stroke", "#cc7918");
line.setAttribute("stroke-width", "2.6");
line.setAttribute("stroke-linecap", "round");
line.setAttribute("stroke-linejoin", "round");
svg.appendChild(line);
// animate line draw
var len = line.getTotalLength();
line.style.strokeDasharray = len;
line.style.strokeDashoffset = len;
line.getBoundingClientRect();
line.style.transition = "stroke-dashoffset 0.8s ease";
line.style.strokeDashoffset = "0";
// interactive dots
data.forEach(function (v, i) {
var c = document.createElementNS(SVGNS, "circle");
c.setAttribute("cx", x(i)); c.setAttribute("cy", y(v));
c.setAttribute("r", "4.5");
c.setAttribute("fill", "#fff");
c.setAttribute("stroke", "#cc7918");
c.setAttribute("stroke-width", "2.2");
c.setAttribute("class", "chart-dot");
c.setAttribute("tabindex", "0");
var pct = Math.round((v / CAPACITY) * 100);
var label = HOURS[i] + ":00 · <b>" + v + "</b> seats · " + pct + "%";
function show() {
chartTip.hidden = false;
chartTip.innerHTML = label;
var rect = svg.getBoundingClientRect();
var rel = rect.width / W;
chartTip.style.left = (x(i) * rel) + "px";
chartTip.style.top = (y(v) * (rect.height / H) - 12) + "px";
c.setAttribute("r", "6.5");
}
function hide() { chartTip.hidden = true; c.setAttribute("r", "4.5"); }
c.addEventListener("mouseenter", show);
c.addEventListener("focus", show);
c.addEventListener("mouseleave", hide);
c.addEventListener("blur", hide);
svg.appendChild(c);
});
// x labels
var xr = document.getElementById("chartX");
xr.innerHTML = "";
[0, 3, 6, 9, 12, 13].forEach(function (i) {
var s = document.createElement("span");
s.textContent = HOURS[i] + ":00";
xr.appendChild(s);
});
document.getElementById("chartSub").textContent = cfg.sub;
}
/* ---------- Underused resources ---------- */
var UNDER = [
{ name: "Maker Lab", meta: "12 seats · weekday afternoons", pct: 21, ico: "⚒" },
{ name: "Phone Booth · B3", meta: "Single occupancy · all day", pct: 28, ico: "☏" },
{ name: "Meeting Room · Birch", meta: "8 seats · most underbooked", pct: 34, ico: "◷" },
{ name: "Garden Lounge", meta: "24 seats · mornings only", pct: 41, ico: "❧" }
];
function renderUnder() {
var ul = document.getElementById("underList");
ul.innerHTML = "";
UNDER.forEach(function (r) {
var li = document.createElement("li");
li.className = "under-item";
var warn = r.pct >= 40 ? " warn" : "";
li.innerHTML =
'<span class="ui-ico">' + r.ico + '</span>' +
'<div class="ui-body"><div class="ui-name">' + r.name + '</div>' +
'<div class="ui-meta">' + r.meta + '</div></div>' +
'<div class="ui-use"><div class="ui-pct' + warn + '">' + r.pct + '%</div>' +
'<div class="ui-track"><i style="width:' + r.pct + '%"></i></div></div>';
li.addEventListener("click", function () {
toast("Flagged " + r.name + " for the utilization review");
});
ul.appendChild(li);
});
}
/* ---------- Heatmap ---------- */
function heatColor(v) {
// v: 0..1 ; concrete -> amber -> danger
var stops = [
[245, 239, 230], [246, 207, 154], [232, 144, 43], [212, 80, 62]
];
var seg = v * (stops.length - 1);
var i = Math.min(stops.length - 2, Math.floor(seg));
var f = seg - i;
var a = stops[i], b = stops[i + 1];
var r = Math.round(a[0] + (b[0] - a[0]) * f);
var g = Math.round(a[1] + (b[1] - a[1]) * f);
var bl = Math.round(a[2] + (b[2] - a[2]) * f);
return "rgb(" + r + "," + g + "," + bl + ")";
}
var heatHours = ["08", "10", "12", "14", "16", "18"];
var heatData = {}; // zoneId -> [values per heatHour]
function buildHeatData(cfg) {
var rnd = seeded(cfg.seed + 9);
var hourBias = [0.45, 0.85, 0.7, 0.9, 0.6, 0.3];
heatData = {};
ZONES.forEach(function (z, zi) {
var zoneBias = 0.55 + (zi % 3) * 0.12;
heatData[z.id] = hourBias.map(function (hb) {
var v = hb * zoneBias * cfg.amp + (rnd() - 0.5) * 0.14;
return Math.max(0.05, Math.min(0.99, v));
});
});
}
function renderHeat(cfg) {
buildHeatData(cfg);
var heat = document.getElementById("heat");
heat.style.gridTemplateColumns = "minmax(120px,1.4fr) repeat(" + heatHours.length + ", 1fr)";
heat.innerHTML = "";
// header row
var blank = document.createElement("div");
heat.appendChild(blank);
heatHours.forEach(function (h) {
var hd = document.createElement("div");
hd.className = "heat-head";
hd.textContent = h + ":00";
heat.appendChild(hd);
});
ZONES.forEach(function (z) {
var label = document.createElement("div");
label.className = "heat-label zone";
label.textContent = z.name;
label.setAttribute("tabindex", "0");
label.setAttribute("role", "button");
label.addEventListener("click", function () { drill(z); });
label.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); drill(z); }
});
heat.appendChild(label);
heatData[z.id].forEach(function (v, hi) {
var cell = document.createElement("div");
cell.className = "cell";
cell.style.background = heatColor(v);
cell.setAttribute("tabindex", "0");
var pct = Math.round(v * 100);
var seats = Math.round(v * z.seats);
cell.setAttribute("aria-label", z.name + " at " + heatHours[hi] + ":00, " + pct + " percent occupied");
cell.setAttribute("role", "gridcell");
function show() {
chartTip.hidden = false;
chartTip.innerHTML = z.name + " · " + heatHours[hi] + ":00<br><b>" + pct + "%</b> · " + seats + "/" + z.seats + " seats";
var rect = cell.getBoundingClientRect();
var host = document.querySelector(".heat-panel").getBoundingClientRect();
chartTip.style.left = (rect.left - host.left + rect.width / 2) + "px";
chartTip.style.top = (rect.top - host.top - 8) + "px";
}
cell.addEventListener("mouseenter", show);
cell.addEventListener("focus", show);
cell.addEventListener("mouseleave", function () { chartTip.hidden = true; });
cell.addEventListener("blur", function () { chartTip.hidden = true; });
cell.addEventListener("click", function () { drill(z); });
heat.appendChild(cell);
});
});
}
/* ---------- Zone drill-in ---------- */
var drillEl = document.getElementById("drill");
function drill(z) {
var vals = heatData[z.id];
var avg = vals.reduce(function (a, b) { return a + b; }, 0) / vals.length;
var peakIdx = vals.indexOf(Math.max.apply(null, vals));
var peakSeats = Math.round(vals[peakIdx] * z.seats);
var lowIdx = vals.indexOf(Math.min.apply(null, vals));
document.getElementById("drillTitle").textContent = z.name + " — zone detail";
document.getElementById("drillStats").innerHTML =
stat(z.seats, "Capacity") +
stat(Math.round(avg * 100) + "%", "Avg. occupancy") +
stat(heatHours[peakIdx] + ":00", "Peak · " + peakSeats + " seats") +
stat(heatHours[lowIdx] + ":00", "Quietest hour");
drillEl.hidden = false;
drillEl.scrollIntoView({ behavior: "smooth", block: "nearest" });
toast("Drilled into " + z.name);
}
function stat(val, lab) {
return '<div class="ds-item"><div class="ds-val">' + val + '</div><div class="ds-lab">' + lab + '</div></div>';
}
document.getElementById("drillClose").addEventListener("click", function () {
drillEl.hidden = true;
});
/* ---------- Timeframe toggle ---------- */
var segBtns = document.querySelectorAll(".seg-btn");
function selectRange(range) {
var cfg = RANGES[range];
renderKpis(cfg);
renderSpark(cfg);
renderChart(cfg);
renderHeat(cfg);
drillEl.hidden = true;
}
segBtns.forEach(function (btn) {
btn.addEventListener("click", function () {
segBtns.forEach(function (b) {
b.classList.remove("is-active");
b.setAttribute("aria-selected", "false");
});
btn.classList.add("is-active");
btn.setAttribute("aria-selected", "true");
var r = btn.getAttribute("data-range");
selectRange(r);
toast("Showing " + btn.textContent.toLowerCase() + " view");
});
});
/* ---------- Init ---------- */
renderAvatars();
renderUnder();
selectRange("today");
// keep chart/heat responsive on resize
var rt;
window.addEventListener("resize", function () {
clearTimeout(rt);
rt = setTimeout(function () {
var active = document.querySelector(".seg-btn.is-active").getAttribute("data-range");
renderChart(RANGES[active]);
}, 180);
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Loftworks — Occupancy 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">
<!-- Sidebar -->
<aside class="sidebar" aria-label="Primary">
<div class="brand">
<span class="brand-mark" aria-hidden="true">◧</span>
<span class="brand-name">Loftworks</span>
</div>
<nav class="nav" aria-label="Sections">
<a class="nav-item is-active" href="#" aria-current="page"><span class="ni-ico">▦</span> Occupancy</a>
<a class="nav-item" href="#"><span class="ni-ico">▥</span> Floor plan</a>
<a class="nav-item" href="#"><span class="ni-ico">◷</span> Bookings</a>
<a class="nav-item" href="#"><span class="ni-ico">◍</span> Members</a>
<a class="nav-item" href="#"><span class="ni-ico">⚙</span> Settings</a>
</nav>
<div class="sidebar-foot">
<div class="live-dot" aria-hidden="true"></div>
<span>Live · synced 1m ago</span>
</div>
</aside>
<!-- Main -->
<main class="main">
<header class="topbar">
<div>
<p class="eyebrow">Riverside Studio · Floor 2</p>
<h1>Occupancy & Utilization</h1>
</div>
<div class="topbar-actions">
<div class="seg" role="tablist" aria-label="Timeframe">
<button class="seg-btn is-active" role="tab" aria-selected="true" data-range="today">Today</button>
<button class="seg-btn" role="tab" aria-selected="false" data-range="week">Week</button>
<button class="seg-btn" role="tab" aria-selected="false" data-range="month">Month</button>
</div>
<div class="avatar" title="Mara Quintero, Space Manager" aria-hidden="true">MQ</div>
</div>
</header>
<!-- KPIs -->
<section class="kpis" aria-label="Key metrics">
<article class="kpi">
<div class="kpi-top">
<span class="kpi-label">Occupancy</span>
<span class="trend up" id="trendOcc">▲ 6%</span>
</div>
<div class="kpi-value"><span id="kpiOcc">72</span><span class="unit">%</span></div>
<div class="bar"><span class="bar-fill" id="kpiOccBar" style="width:72%"></span></div>
<p class="kpi-sub">112 of 156 seats in use</p>
</article>
<article class="kpi">
<div class="kpi-top">
<span class="kpi-label">Desk utilization</span>
<span class="trend up" id="trendUtil">▲ 3%</span>
</div>
<div class="kpi-value"><span id="kpiUtil">64</span><span class="unit">%</span></div>
<div class="bar"><span class="bar-fill plant" id="kpiUtilBar" style="width:64%"></span></div>
<p class="kpi-sub">Avg. across the working day</p>
</article>
<article class="kpi">
<div class="kpi-top">
<span class="kpi-label">Peak hour</span>
<span class="trend flat" id="trendPeak">— steady</span>
</div>
<div class="kpi-value"><span id="kpiPeak">11:00</span></div>
<div class="spark" id="kpiSpark" aria-hidden="true"></div>
<p class="kpi-sub">88% of seats at busiest</p>
</article>
<article class="kpi">
<div class="kpi-top">
<span class="kpi-label">Members checked in</span>
<span class="trend up" id="trendMem">▲ 12</span>
</div>
<div class="kpi-value"><span id="kpiMem">128</span></div>
<div class="avatars" id="kpiAvatars" aria-hidden="true"></div>
<p class="kpi-sub">of 214 active members</p>
</article>
</section>
<div class="grid">
<!-- Occupancy over day chart -->
<section class="panel chart-panel" aria-label="Occupancy over the day">
<div class="panel-head">
<div>
<h2>Occupancy over the day</h2>
<p class="panel-sub" id="chartSub">Live seats in use · 07:00–20:00</p>
</div>
<div class="legend">
<span class="lg"><i class="dot amber"></i> In use</span>
<span class="lg"><i class="dot line"></i> Capacity</span>
</div>
</div>
<div class="chart">
<svg id="chartSvg" viewBox="0 0 720 240" preserveAspectRatio="none" role="img" aria-label="Line chart of occupancy through the day"></svg>
<div class="chart-x" id="chartX"></div>
<div class="chart-tip" id="chartTip" hidden></div>
</div>
</section>
<!-- Underused resources -->
<section class="panel" aria-label="Underused resources">
<div class="panel-head">
<h2>Underused this week</h2>
<span class="pill warn">Review</span>
</div>
<ul class="under-list" id="underList"></ul>
</section>
</div>
<!-- Heatmap -->
<section class="panel heat-panel" aria-label="Occupancy heatmap by zone and hour">
<div class="panel-head">
<div>
<h2>Heatmap · zone × hour</h2>
<p class="panel-sub">Hover a cell for detail · click a zone to drill in</p>
</div>
<div class="legend heat-legend">
<span class="lg">Low</span>
<span class="scale" aria-hidden="true"></span>
<span class="lg">High</span>
</div>
</div>
<div class="heat-wrap">
<div class="heat" id="heat" role="grid" aria-label="Heatmap grid"></div>
</div>
<div class="drill" id="drill" hidden>
<button class="drill-close" id="drillClose" aria-label="Close zone detail">✕</button>
<h3 id="drillTitle">Zone</h3>
<div class="drill-stats" id="drillStats"></div>
</div>
</section>
</main>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Occupancy Dashboard
A space-manager’s home screen for Loftworks, a fictional coworking studio. The header carries the venue, an avatar and a pill-style timeframe toggle. Four KPI cards animate their values on load and on change — occupancy with a fill bar, desk utilization in plant green, the peak hour with a tiny sparkline, and checked-in members shown as a stack of overlapping avatars. Each card pairs a value with a coloured trend chip so the direction of travel reads at a glance.
Below, an SVG occupancy-over-day chart draws its line with a stroke animation and fills a soft amber gradient beneath it; hovering or keyboard-focusing any point reveals a tooltip with the exact seat count and percentage against capacity. To the right, an “underused this week” list ranks the quietest resources, each flagged for review on click. The centrepiece is a zone × hour heatmap whose cells warm from concrete through amber to danger red; hovering a cell shows seats-in-use, and clicking a cell or its zone label drills into a four-stat breakdown of capacity, average occupancy, peak and quietest hour.
Everything is deterministic and self-contained: the timeframe toggle reseeds the data so today, week and month each tell a consistent, believable story, with a toast confirming each interaction. The layout collapses the sidebar to a top bar and stacks the cards down to roughly 360px wide, and all controls are keyboard-usable with aria labels.
Illustrative UI only — fictional coworking space, not a real booking system.