Science — Interactive Figure (toggle series)
A journal-grade interactive figure drawn entirely with inline SVG and vanilla JS — no charting library. It plots three abyssal monitoring stations as smoothed line-plus-marker series over sea-surface temperature, with gridlines, axis titles, units and bootstrap 95% confidence whiskers. A clickable legend toggles each station with smooth fades, a segmented control switches the y-axis between linear and log scale, and per-point hover tooltips read out temperature, NPP and CI. Seeded data renders identically offline.
MCP
Code
:root {
--bg: #ffffff;
--bg-alt: #f6f8fb;
--ink: #0f1b2d;
--ink-2: #33445c;
--muted: #697892;
--accent: #1a4f8a;
--accent-d: #123a66;
--accent-50: #e9f0f9;
--teal: #0f7d78;
--teal-50: #e4f3f1;
--line: rgba(15, 27, 45, 0.12);
--line-2: rgba(15, 27, 45, 0.2);
--ok: #2f9e6f;
--warn: #c9821f;
--danger: #cf4538;
--r-sm: 6px;
--r-md: 10px;
--r-lg: 16px;
--sh-1: 0 1px 2px rgba(15, 27, 45, 0.06), 0 1px 3px rgba(15, 27, 45, 0.05);
--sh-2: 0 8px 28px rgba(15, 27, 45, 0.1);
/* series colors */
--s1: #1a4f8a;
--s2: #0f7d78;
--s3: #c9821f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: "Source Serif 4", Georgia, serif;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.mono {
font-family: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
font-variant-numeric: tabular-nums;
letter-spacing: -0.01em;
}
.wrap {
max-width: 880px;
margin: 0 auto;
padding: 48px 24px 72px;
}
/* ---------- masthead ---------- */
.masthead {
border-bottom: 1px solid var(--line);
padding-bottom: 24px;
margin-bottom: 28px;
}
.eyebrow {
font-family: "Inter", system-ui, sans-serif;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--teal);
margin: 0 0 12px;
}
.masthead h1 {
font-size: clamp(1.5rem, 1.1rem + 1.6vw, 2.1rem);
line-height: 1.25;
font-weight: 700;
letter-spacing: -0.01em;
margin: 0 0 14px;
max-width: 36ch;
}
.byline {
margin: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.byline .authors {
font-weight: 600;
color: var(--ink-2);
}
.byline .affil {
font-family: "Inter", system-ui, sans-serif;
font-size: 13px;
color: var(--muted);
}
.byline abbr {
text-decoration: none;
border-bottom: 1px dotted var(--line-2);
}
/* ---------- panel ---------- */
.panel {
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-1);
padding: 22px;
}
.panel-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 14px;
}
.panel-head h2 {
font-family: "Inter", system-ui, sans-serif;
font-size: 15px;
font-weight: 700;
letter-spacing: 0.01em;
margin: 0;
color: var(--ink);
}
/* segmented control */
.seg {
display: inline-flex;
align-items: center;
gap: 4px;
background: var(--bg-alt);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 3px;
}
.seg-label {
font-family: "Inter", system-ui, sans-serif;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
padding: 0 6px 0 4px;
}
.seg-btn {
font-family: "Inter", system-ui, sans-serif;
font-size: 13px;
font-weight: 600;
color: var(--ink-2);
background: transparent;
border: 0;
border-radius: var(--r-sm);
padding: 5px 12px;
cursor: pointer;
transition: background 0.16s ease, color 0.16s ease, box-shadow 0.16s ease;
}
.seg-btn:hover {
color: var(--accent);
}
.seg-btn.is-active {
background: var(--bg);
color: var(--accent-d);
box-shadow: var(--sh-1);
}
/* toolbar */
.toolbar {
display: flex;
align-items: center;
gap: 18px;
flex-wrap: wrap;
padding: 10px 0 16px;
border-bottom: 1px dashed var(--line);
margin-bottom: 16px;
}
.chk {
display: inline-flex;
align-items: center;
gap: 8px;
font-family: "Inter", system-ui, sans-serif;
font-size: 13px;
color: var(--ink-2);
cursor: pointer;
user-select: none;
}
.chk input {
width: 16px;
height: 16px;
accent-color: var(--accent);
cursor: pointer;
}
.btn-ghost {
margin-left: auto;
font-family: "Inter", system-ui, sans-serif;
font-size: 13px;
font-weight: 600;
color: var(--accent);
background: transparent;
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 6px 14px;
cursor: pointer;
transition: background 0.16s ease, border-color 0.16s ease;
}
.btn-ghost:hover {
background: var(--accent-50);
border-color: var(--line-2);
}
/* ---------- figure ---------- */
.figure {
margin: 0;
}
.plot-scroll {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.plot-stage {
position: relative;
min-width: 460px;
}
#plot {
display: block;
width: 100%;
height: auto;
font-family: "JetBrains Mono", ui-monospace, monospace;
}
/* svg primitives (styled via classes added in JS) */
.grid-line {
stroke: var(--line);
stroke-width: 1;
}
.grid-line.minor {
stroke: rgba(15, 27, 45, 0.06);
}
.axis-line {
stroke: var(--line-2);
stroke-width: 1.5;
}
.tick-label {
fill: var(--muted);
font-size: 11px;
}
.axis-title {
fill: var(--ink-2);
font-family: "Inter", system-ui, sans-serif;
font-size: 12px;
font-weight: 600;
}
.series-line {
fill: none;
stroke-width: 2.25;
stroke-linejoin: round;
stroke-linecap: round;
transition: opacity 0.32s ease, stroke-dashoffset 0.6s ease;
}
.series-dot {
transition: opacity 0.28s ease, r 0.12s ease;
cursor: pointer;
}
.series-dot:hover {
r: 6;
}
.err-bar {
stroke-width: 1.4;
transition: opacity 0.28s ease;
}
.series-hidden {
opacity: 0 !important;
pointer-events: none;
}
.fade-soft {
transition: opacity 0.3s ease;
}
/* legend */
.legend {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 8px 10px;
padding: 16px 0 4px;
margin: 0;
}
.legend li {
display: flex;
}
.legend-btn {
display: inline-flex;
align-items: center;
gap: 8px;
font-family: "Inter", system-ui, sans-serif;
font-size: 12.5px;
font-weight: 600;
color: var(--ink-2);
background: var(--bg-alt);
border: 1px solid var(--line);
border-radius: 999px;
padding: 5px 13px 5px 11px;
cursor: pointer;
transition: background 0.16s ease, border-color 0.16s ease, opacity 0.2s ease, color 0.16s ease;
}
.legend-btn:hover {
border-color: var(--line-2);
color: var(--ink);
}
.legend-swatch {
width: 16px;
height: 3px;
border-radius: 2px;
flex: none;
position: relative;
}
.legend-swatch::after {
content: "";
position: absolute;
width: 7px;
height: 7px;
border-radius: 50%;
background: inherit;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.legend-btn[aria-pressed="false"] {
opacity: 0.45;
text-decoration: line-through;
text-decoration-thickness: 1px;
}
.legend-btn:focus-visible,
.seg-btn:focus-visible,
.btn-ghost:focus-visible,
.chk input:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
/* caption */
.caption {
font-family: "Inter", system-ui, sans-serif;
font-size: 12.5px;
line-height: 1.6;
color: var(--muted);
border-top: 1px solid var(--line);
margin-top: 14px;
padding-top: 12px;
max-width: 64ch;
}
.fig-num {
font-weight: 700;
color: var(--ink-2);
}
/* tooltip */
.tooltip {
position: absolute;
pointer-events: none;
z-index: 5;
min-width: 130px;
background: var(--ink);
color: #fff;
border-radius: var(--r-sm);
padding: 8px 10px;
box-shadow: var(--sh-2);
font-family: "JetBrains Mono", ui-monospace, monospace;
font-size: 11.5px;
line-height: 1.5;
transform: translate(-50%, calc(-100% - 12px));
transition: opacity 0.12s ease;
}
.tooltip[hidden] {
display: none;
}
.tooltip::after {
content: "";
position: absolute;
left: 50%;
bottom: -5px;
width: 9px;
height: 9px;
background: var(--ink);
transform: translateX(-50%) rotate(45deg);
}
.tt-head {
font-family: "Inter", system-ui, sans-serif;
font-weight: 600;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 3px;
display: flex;
align-items: center;
gap: 6px;
}
.tt-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.tt-row span {
color: rgba(255, 255, 255, 0.66);
}
/* ---------- notes ---------- */
.notes {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
margin-top: 24px;
}
.note-card {
background: var(--bg-alt);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 16px;
}
.note-card h3 {
font-family: "Inter", system-ui, sans-serif;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--teal);
margin: 0 0 8px;
}
.note-card p {
font-size: 14px;
margin: 0;
color: var(--ink-2);
}
.eq {
font-style: italic;
white-space: nowrap;
}
.eq sup,
.eq sub {
font-style: normal;
}
/* ---------- toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translateX(-50%) translateY(16px);
background: var(--ink);
color: #fff;
font-family: "Inter", system-ui, sans-serif;
font-size: 13px;
font-weight: 500;
padding: 10px 18px;
border-radius: 999px;
box-shadow: var(--sh-2);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s ease, transform 0.22s ease;
z-index: 50;
}
.toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* ---------- responsive ---------- */
@media (max-width: 640px) {
.wrap {
padding: 32px 16px 56px;
}
.panel {
padding: 16px;
}
.panel-head {
align-items: flex-start;
}
.toolbar {
gap: 12px 16px;
}
.btn-ghost {
margin-left: 0;
}
.notes {
grid-template-columns: 1fr;
}
}(function () {
"use strict";
var SVGNS = "http://www.w3.org/2000/svg";
/* ---------- toast helper ---------- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2200);
}
/* ---------- seeded deterministic data ---------- */
// tiny mulberry32 PRNG so the figure renders identically every load
function rng(seed) {
return function () {
seed |= 0;
seed = (seed + 0x6d2b79f5) | 0;
var t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
// Modified Eppley curve: P = P0 * exp(k * (T - T0)), with noise + CI
function buildSeries(label, color, p0, k, seed) {
var rand = rng(seed);
var T0 = 4;
var pts = [];
for (var T = 0; T <= 18; T += 2) {
var mean = p0 * Math.exp(k * (T - T0));
// multiplicative noise so means stay positive
var noise = 1 + (rand() - 0.5) * 0.22;
var y = mean * noise;
var ci = y * (0.08 + rand() * 0.12); // half-width of 95% CI
pts.push({ x: T, y: Math.max(1, y), ci: ci });
}
return { label: label, color: color, points: pts, visible: true };
}
var series = [
buildSeries("S-12 Drake Passage", "var(--s1)", 95, 0.135, 1337),
buildSeries("S-07 Weddell Gyre", "var(--s2)", 60, 0.165, 8042),
buildSeries("S-21 Kerguelen Plateau", "var(--s3)", 140, 0.092, 5519)
];
/* ---------- plot geometry ---------- */
var W = 720,
H = 440;
var M = { top: 24, right: 22, bottom: 56, left: 64 };
var iw = W - M.left - M.right;
var ih = H - M.top - M.bottom;
var xDomain = [0, 18]; // temperature °C
var state = {
scale: "linear",
errorBars: true,
markers: true
};
var svg = document.getElementById("plot");
var legendEl = document.getElementById("legend");
var tooltip = document.getElementById("tooltip");
var stage = svg.parentNode;
function el(name, attrs) {
var e = document.createElementNS(SVGNS, name);
if (attrs) {
for (var k in attrs) e.setAttribute(k, attrs[k]);
}
return e;
}
/* ---------- scales ---------- */
function yExtent() {
var max = 0,
min = Infinity;
series.forEach(function (s) {
if (!s.visible) return;
s.points.forEach(function (p) {
max = Math.max(max, p.y + p.ci);
min = Math.min(min, Math.max(1, p.y - p.ci));
});
});
if (max === 0) {
max = 100;
min = 1;
}
return [min, max];
}
function xScale(v) {
return M.left + ((v - xDomain[0]) / (xDomain[1] - xDomain[0])) * iw;
}
function makeYScale() {
var ext = yExtent();
if (state.scale === "log") {
var lo = Math.log10(Math.max(1, ext[0]));
var hi = Math.log10(ext[1] * 1.05);
lo = Math.floor(lo);
hi = Math.ceil(hi);
return {
fn: function (v) {
var lv = Math.log10(Math.max(1, v));
return M.top + ih - ((lv - lo) / (hi - lo)) * ih;
},
ticks: (function () {
var t = [];
for (var e = lo; e <= hi; e++) t.push(Math.pow(10, e));
return t;
})(),
fmt: function (v) {
return v >= 1000 ? v / 1000 + "k" : "" + v;
}
};
}
var top = Math.ceil((ext[1] * 1.08) / 50) * 50;
var bottom = 0;
var ticks = [];
var step = top / 5;
for (var i = 0; i <= 5; i++) ticks.push(bottom + step * i);
return {
fn: function (v) {
return M.top + ih - ((v - bottom) / (top - bottom)) * ih;
},
ticks: ticks,
fmt: function (v) {
return v >= 1000 ? (v / 1000).toFixed(1) + "k" : Math.round(v);
}
};
}
/* ---------- render ---------- */
function render() {
while (svg.firstChild) svg.removeChild(svg.firstChild);
var y = makeYScale();
// gridlines + y ticks
y.ticks.forEach(function (tv) {
var yy = y.fn(tv);
svg.appendChild(
el("line", {
class: "grid-line",
x1: M.left,
x2: M.left + iw,
y1: yy,
y2: yy
})
);
var lbl = el("text", {
class: "tick-label",
x: M.left - 10,
y: yy + 3.5,
"text-anchor": "end"
});
lbl.textContent = y.fmt(tv);
svg.appendChild(lbl);
});
// x ticks (every 2 °C)
for (var xv = xDomain[0]; xv <= xDomain[1]; xv += 2) {
var xx = xScale(xv);
svg.appendChild(
el("line", {
class: "grid-line minor",
x1: xx,
x2: xx,
y1: M.top,
y2: M.top + ih
})
);
var xl = el("text", {
class: "tick-label",
x: xx,
y: M.top + ih + 20,
"text-anchor": "middle"
});
xl.textContent = xv;
svg.appendChild(xl);
}
// axes
svg.appendChild(
el("line", { class: "axis-line", x1: M.left, x2: M.left, y1: M.top, y2: M.top + ih })
);
svg.appendChild(
el("line", {
class: "axis-line",
x1: M.left,
x2: M.left + iw,
y1: M.top + ih,
y2: M.top + ih
})
);
// axis titles
var xt = el("text", {
class: "axis-title",
x: M.left + iw / 2,
y: H - 12,
"text-anchor": "middle"
});
xt.textContent = "Sea-surface temperature (°C)";
svg.appendChild(xt);
var yt = el("text", {
class: "axis-title",
x: 16,
y: M.top + ih / 2,
"text-anchor": "middle",
transform: "rotate(-90 16 " + (M.top + ih / 2) + ")"
});
yt.textContent =
"NPP (mg C m⁻² d⁻¹" + (state.scale === "log" ? ", log₁₀" : "") + ")";
svg.appendChild(yt);
// series
series.forEach(function (s, si) {
var g = el("g", { class: "fade-soft" });
g.setAttribute("data-series", si);
if (!s.visible) g.classList.add("series-hidden");
// line path
var d = "";
s.points.forEach(function (p, i) {
d += (i === 0 ? "M" : "L") + xScale(p.x) + " " + y.fn(p.y) + " ";
});
var path = el("path", { class: "series-line", d: d, stroke: s.color });
g.appendChild(path);
// error bars
s.points.forEach(function (p) {
var px = xScale(p.x);
var yTop = y.fn(p.y + p.ci);
var yBot = y.fn(Math.max(1, p.y - p.ci));
var bar = el("g", { class: "err-bar-group" });
bar.style.display = state.errorBars ? "" : "none";
bar.classList.add("err-bar-wrap");
bar.appendChild(
el("line", { class: "err-bar", x1: px, x2: px, y1: yTop, y2: yBot, stroke: s.color })
);
bar.appendChild(
el("line", { class: "err-bar", x1: px - 4, x2: px + 4, y1: yTop, y2: yTop, stroke: s.color })
);
bar.appendChild(
el("line", { class: "err-bar", x1: px - 4, x2: px + 4, y1: yBot, y2: yBot, stroke: s.color })
);
g.appendChild(bar);
});
// markers
s.points.forEach(function (p) {
var dot = el("circle", {
class: "series-dot",
cx: xScale(p.x),
cy: y.fn(p.y),
r: 4,
fill: "#fff",
stroke: s.color,
"stroke-width": 2
});
dot.style.display = state.markers ? "" : "none";
dot.setAttribute("tabindex", "0");
dot.setAttribute(
"aria-label",
s.label + ": " + p.x + " °C, NPP " + p.y.toFixed(1) + " ± " + p.ci.toFixed(1)
);
dot.addEventListener("mouseenter", function (ev) {
showTip(ev, s, p);
});
dot.addEventListener("focus", function (ev) {
showTip(ev, s, p);
});
dot.addEventListener("mouseleave", hideTip);
dot.addEventListener("blur", hideTip);
g.appendChild(dot);
});
svg.appendChild(g);
});
}
/* ---------- tooltip ---------- */
function resolveColor(c) {
if (c.indexOf("var(") !== 0) return c;
var name = c.slice(4, -1).trim();
return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || "#1a4f8a";
}
function showTip(ev, s, p) {
var rect = stage.getBoundingClientRect();
var dot = ev.target;
var dr = dot.getBoundingClientRect();
var col = resolveColor(s.color);
tooltip.innerHTML =
'<div class="tt-head"><span class="tt-dot" style="background:' +
col +
'"></span>' +
s.label +
"</div>" +
'<div class="tt-row"><span>T =</span> ' +
p.x.toFixed(1) +
" °C</div>" +
'<div class="tt-row"><span>NPP =</span> ' +
p.y.toFixed(1) +
" mg C m⁻² d⁻¹</div>" +
'<div class="tt-row"><span>95% CI</span> ±' +
p.ci.toFixed(1) +
"</div>";
tooltip.hidden = false;
var cx = dr.left - rect.left + dr.width / 2;
var cy = dr.top - rect.top + dr.height / 2;
tooltip.style.left = cx + "px";
tooltip.style.top = cy + "px";
}
function hideTip() {
tooltip.hidden = true;
}
/* ---------- legend ---------- */
function buildLegend() {
legendEl.innerHTML = "";
series.forEach(function (s, si) {
var li = document.createElement("li");
var btn = document.createElement("button");
btn.type = "button";
btn.className = "legend-btn";
btn.setAttribute("aria-pressed", s.visible ? "true" : "false");
btn.innerHTML =
'<span class="legend-swatch" style="background:' +
resolveColor(s.color) +
'"></span>' +
s.label;
btn.addEventListener("click", function () {
s.visible = !s.visible;
btn.setAttribute("aria-pressed", s.visible ? "true" : "false");
var visCount = series.filter(function (x) {
return x.visible;
}).length;
if (visCount === 0) {
// never allow an empty plot
s.visible = true;
btn.setAttribute("aria-pressed", "true");
toast("At least one station must stay visible");
return;
}
hideTip();
render();
toast((s.visible ? "Showing " : "Hidden ") + s.label);
});
li.appendChild(btn);
legendEl.appendChild(li);
});
}
/* ---------- controls ---------- */
var scaleBtns = document.querySelectorAll(".seg-btn");
scaleBtns.forEach(function (b) {
b.addEventListener("click", function () {
if (b.classList.contains("is-active")) return;
scaleBtns.forEach(function (x) {
x.classList.remove("is-active");
x.setAttribute("aria-pressed", "false");
});
b.classList.add("is-active");
b.setAttribute("aria-pressed", "true");
state.scale = b.getAttribute("data-scale");
hideTip();
render();
toast("Y-axis: " + (state.scale === "log" ? "log₁₀ scale" : "linear scale"));
});
});
var errToggle = document.getElementById("errToggle");
errToggle.addEventListener("change", function () {
state.errorBars = errToggle.checked;
render();
});
var ptToggle = document.getElementById("ptToggle");
ptToggle.addEventListener("change", function () {
state.markers = ptToggle.checked;
render();
});
document.getElementById("resetBtn").addEventListener("click", function () {
state.scale = "linear";
state.errorBars = true;
state.markers = true;
series.forEach(function (s) {
s.visible = true;
});
errToggle.checked = true;
ptToggle.checked = true;
scaleBtns.forEach(function (x) {
var on = x.getAttribute("data-scale") === "linear";
x.classList.toggle("is-active", on);
x.setAttribute("aria-pressed", on ? "true" : "false");
});
buildLegend();
hideTip();
render();
toast("View reset");
});
/* ---------- init ---------- */
buildLegend();
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Interactive Figure — Toggle Series</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&family=JetBrains+Mono:wght@400;500&family=Source+Serif+4:opsz,[email protected],400;8..60,600;8..60,700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="wrap">
<header class="masthead">
<p class="eyebrow">Marine Biogeochemistry · Open Data Supplement</p>
<h1>Temperature-dependence of phytoplankton net primary production across three abyssal monitoring stations</h1>
<p class="byline">
<span class="authors">A. Rourke-Vance, J. Oduya, P. Lindqvist & the <abbr title="Fictional consortium">DEEPTRACE</abbr> Consortium</span>
<span class="affil">Institute for Pelagic Systems, Université de Tharsis · Preprint <span class="mono">arXiv:2606.04417</span></span>
</p>
</header>
<section class="panel" aria-labelledby="figtitle">
<div class="panel-head">
<h2 id="figtitle">Interactive Figure</h2>
<div class="seg" role="group" aria-label="Y-axis scale">
<span class="seg-label">Y scale</span>
<button type="button" class="seg-btn is-active" data-scale="linear" aria-pressed="true">Linear</button>
<button type="button" class="seg-btn" data-scale="log" aria-pressed="false">Log₁₀</button>
</div>
</div>
<div class="toolbar">
<label class="chk">
<input type="checkbox" id="errToggle" checked />
<span>Error bars (95% CI)</span>
</label>
<label class="chk">
<input type="checkbox" id="ptToggle" checked />
<span>Data markers</span>
</label>
<button type="button" class="btn-ghost" id="resetBtn">Reset view</button>
</div>
<figure class="figure">
<div class="plot-scroll">
<div class="plot-stage">
<svg id="plot" viewBox="0 0 720 440" role="img"
aria-label="Multi-series line and scatter plot of net primary production versus sea-surface temperature for three stations.">
<!-- rendered by script.js -->
</svg>
<div id="tooltip" class="tooltip" role="status" aria-live="polite" hidden></div>
</div>
</div>
<ul class="legend" id="legend" aria-label="Series toggles — click to show or hide">
<!-- rendered by script.js -->
</ul>
<figcaption class="caption">
<span class="fig-num">Figure 3.</span>
Sea-surface temperature versus depth-integrated net primary production
(<span class="mono">NPP</span>, mg C m⁻² d⁻¹) at three abyssal stations over the
2024–2025 austral season. Markers show seasonal means; whiskers are bootstrap 95% confidence
intervals (<span class="mono">n = 1000</span>). Toggle the y-axis between linear and
log<sub>10</sub> scale, or click a legend entry to hide a station. Source:
DEEPTRACE mooring array, release <span class="mono">v3.2</span>,
<span class="mono">doi:10.5281/zenodo.99421007</span>.
</figcaption>
</figure>
</section>
<section class="notes" aria-label="Methods notes">
<article class="note-card">
<h3>Stations</h3>
<p>Three moorings spanning the polar front: <strong>S-12 Drake Passage</strong>,
<strong>S-07 Weddell Gyre</strong>, and <strong>S-21 Kerguelen Plateau</strong>. Sampling
depth integrated to the 1% light level.</p>
</article>
<article class="note-card">
<h3>Model</h3>
<p>A modified Eppley curve was fit by least squares,
<span class="eq">P = P<sub>0</sub>·<em>e</em><sup>k(T − T₀)</sup></span>,
with <span class="mono">T₀ = 4 °C</span> and station-specific <em>k</em>.</p>
</article>
<article class="note-card">
<h3>Reproducibility</h3>
<p>All series here are seeded deterministically in the client so the figure renders identically
offline. No external charting library is used — axes, gridlines and whiskers are inline SVG.</p>
</article>
</section>
</main>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Interactive Figure (toggle series)
A research-grade scientific figure built without any charting dependency. Axes, gridlines, the line series, the data markers and the 95% confidence whiskers are all generated as inline SVG from seeded data, so the plot looks and behaves the same on every load and works fully offline. Three fictional abyssal stations — Drake Passage, Weddell Gyre and Kerguelen Plateau — are fit to a modified Eppley curve relating net primary production to sea-surface temperature, with units and significant figures set in JetBrains Mono.
The figure is genuinely interactive. The legend is a row of toggle pills: click one to show or hide its station with a smooth opacity transition, and the y-axis automatically rescales to the visible data (the plot refuses to go empty). A segmented control flips the y-axis between linear and log₁₀ scale, regenerating ticks and the axis label. Two checkboxes toggle the error whiskers and the data markers independently, and Reset view restores every default.
Hovering — or keyboard-focusing — any marker raises an accessible tooltip that reads out the exact temperature, NPP value and confidence interval, colour-matched to its series. Everything carries a bold Figure 3. caption with a descriptive source line, fictional DOI and dataset version, and the layout collapses cleanly to a single column with horizontal scroll for the plot below 640px.
Illustrative UI only — fictional authors, data, and figures; not real scientific results.