const HF_REPO_BASE = "https://huggingface.co/datasets";
const LARGE_FILE_BYTES = 15 * 1024 * 1024;
// Pairplot panels grow quadratically; past 12 dims the plot is unreadable
// and canvas rendering gets slow. Cells never shrink below MIN_CELL_PX —
// the canvas grows instead and the wrapper scrolls.
const MAX_DIMS = 12;
const MIN_CELL_PX = 110;
// Canvas colors per theme; the page chrome is themed via CSS variables and
// the [data-theme] attribute, but canvas drawing needs explicit values.
const THEMES = {
light: {
canvasBg: "#ffffff",
text: "#667085",
grid: "#d9deea",
hist: "rgba(49, 91, 232, 0.55)",
point: "rgba(49, 91, 232, 0.25)",
truth: "#e5484d",
},
dark: {
canvasBg: "#1e222d",
text: "#9aa3b5",
grid: "#3a4257",
hist: "rgba(126, 153, 255, 0.6)",
point: "rgba(126, 153, 255, 0.35)",
truth: "#ff6b70",
},
};
const state = {
manifest: null,
task: null,
samples: [],
trueTheta: null,
loadedObservation: null,
themeName: "light",
canvasMessage: "Click Load to fetch posterior samples.",
};
const params = new URLSearchParams(window.location.search);
if (params.get("embedded") === "1") {
document.body.classList.add("embedded");
}
function theme() {
return THEMES[state.themeName] || THEMES.light;
}
function applyTheme(name) {
state.themeName = name === "dark" ? "dark" : "light";
document.documentElement.dataset.theme = state.themeName;
if (state.samples.length) {
drawPlot();
} else {
clearCanvas(state.canvasMessage);
}
}
// Initial theme: explicit ?theme= wins (set by the embedding docs page),
// otherwise follow the OS preference. Only the attribute is set here —
// nothing is drawn yet, and applyTheme would touch elements not yet created.
const themeParam = params.get("theme");
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)");
state.themeName = (themeParam || (prefersDark.matches ? "dark" : "light")) === "dark" ? "dark" : "light";
document.documentElement.dataset.theme = state.themeName;
if (!themeParam) {
prefersDark.addEventListener("change", (event) => applyTheme(event.matches ? "dark" : "light"));
}
// The embedding page notifies us when the docs palette toggles.
window.addEventListener("message", (event) => {
const data = event.data;
if (data && data.type === "sbibm-theme" && (data.theme === "dark" || data.theme === "light")) {
applyTheme(data.theme);
}
});
const els = {
taskSelect: document.getElementById("taskSelect"),
observationSelect: document.getElementById("observationSelect"),
pointLimitSelect: document.getElementById("pointLimitSelect"),
loadButton: document.getElementById("loadButton"),
taskInfo: document.getElementById("taskInfo"),
dimensionGrid: document.getElementById("dimensionGrid"),
status: document.getElementById("status"),
canvas: document.getElementById("plotCanvas"),
};
function formatBytes(bytes) {
if (!Number.isFinite(bytes)) return "unknown size";
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
function taskByName(name) {
return state.manifest.tasks.find((task) => task.task === name) || state.manifest.tasks[0];
}
function hfResolveUrl(path) {
return `${HF_REPO_BASE}/${state.manifest.repo}/resolve/main/${path}`;
}
function hfTreeUrl(path) {
return `${HF_REPO_BASE}/${state.manifest.repo}/tree/main/${path}`;
}
function apiTreeUrl(path) {
return `https://huggingface.co/api/datasets/${state.manifest.repo}/tree/main/${path}`;
}
function setStatus(message) {
els.status.textContent = message;
}
function selectedDims() {
return Array.from(els.dimensionGrid.querySelectorAll("input:checked"))
.map((input) => Number(input.value))
.filter(Number.isInteger)
.slice(0, MAX_DIMS);
}
function renderTaskOptions() {
els.taskSelect.innerHTML = state.manifest.tasks
.map((task) => ``)
.join("");
}
function renderObservationOptions(task) {
const count = task.num_observations || 100;
let html = "";
for (let i = 0; i < count; i += 1) {
html += ``;
}
els.observationSelect.innerHTML = html;
}
function renderTaskInfo(task) {
els.taskInfo.innerHTML = [
`${task.dim_parameters} θ dims`,
`${task.dim_data} x dims`,
`${task.sample_count.toLocaleString()} samples`,
`${formatBytes(task.sample_file_size_bytes)} / observation`,
`Reference potential: ${task.reference_potential_type}`,
`HF files`,
].join(" ");
}
function renderDimensionOptions(task) {
const requested = (params.get("dims") || "")
.split(",")
.map((value) => Number(value.trim()))
.filter((value) => Number.isInteger(value) && value >= 0 && value < task.dim_parameters)
.slice(0, MAX_DIMS);
const defaultCount = Math.min(task.dim_parameters, task.dim_parameters <= 5 ? task.dim_parameters : 4);
const defaults = requested.length >= 2 ? requested : Array.from({ length: defaultCount }, (_, i) => i);
let html = "";
for (let dim = 0; dim < task.dim_parameters; dim += 1) {
const checked = defaults.includes(dim) ? " checked" : "";
html += ``;
}
els.dimensionGrid.innerHTML = html;
els.dimensionGrid.querySelectorAll("input").forEach((input) => {
input.addEventListener("change", () => {
const checkedCount = els.dimensionGrid.querySelectorAll("input:checked").length;
if (input.checked && checkedCount > MAX_DIMS) {
input.checked = false;
setStatus(`At most ${MAX_DIMS} dimensions can be plotted at once.`);
return;
}
drawPlot();
});
});
}
function applyTask(task) {
state.task = task;
state.samples = [];
state.trueTheta = null;
state.loadedObservation = null;
renderObservationOptions(task);
renderTaskInfo(task);
renderDimensionOptions(task);
clearCanvas("Click Load to fetch posterior samples.");
setStatus("Pick an observation, then click Load.");
}
async function importHyparquet() {
return import("https://cdn.jsdelivr.net/npm/hyparquet@1.14.0/+esm");
}
function vectorFromRow(row, name) {
const value = row[name];
if (value == null) return null;
if (Array.isArray(value)) return value.map(Number);
if (ArrayBuffer.isView(value)) return Array.from(value, Number);
if (typeof value === "object") return Object.values(value).map(Number);
return null;
}
function selectParquetFiles(entries) {
const files = entries
.filter((entry) => entry.type === "file" && entry.path.endsWith(".parquet"))
.sort((a, b) => a.path.localeCompare(b.path));
const single = files.find((entry) => /data-00000-of-00001\.parquet$/.test(entry.path));
if (single) return [single];
return files;
}
async function listSampleFiles(task, observationId) {
const path = `${task.task}/reference_samples/observation_${observationId}`;
const response = await fetch(apiTreeUrl(path));
if (!response.ok) throw new Error(`Could not list sample files: HTTP ${response.status}`);
const entries = await response.json();
const files = selectParquetFiles(entries);
if (!files.length) throw new Error("No parquet sample files found for this observation.");
return files;
}
async function readParquetRows(url) {
const { asyncBufferFromUrl, parquetReadObjects } = await importHyparquet();
const file = await asyncBufferFromUrl({ url });
return parquetReadObjects({ file });
}
async function loadSamples(task, observationId) {
const files = await listSampleFiles(task, observationId);
const totalBytes = files.reduce((sum, file) => sum + (file.size || 0), 0);
if (totalBytes > LARGE_FILE_BYTES) {
const ok = window.confirm(
`${task.display_name} observation ${observationId} is ${formatBytes(totalBytes)}. Continue loading it in the browser?`,
);
if (!ok) return null;
}
const samples = [];
for (const file of files) {
const rows = await readParquetRows(hfResolveUrl(file.path));
for (const row of rows) {
const theta = vectorFromRow(row, "theta");
if (theta) samples.push(theta);
}
}
return samples;
}
async function loadTrueTheta(task, observationId) {
try {
const rows = await readParquetRows(hfResolveUrl(`${task.task}/reference_observations/data-00000-of-00001.parquet`));
const row = rows.find((item) => Number(item.observation_id) === observationId) || rows[observationId];
return row ? vectorFromRow(row, "true_theta") : null;
} catch (_) {
return null;
}
}
function downsampleRows(rows, limit) {
if (rows.length <= limit) return rows;
const step = rows.length / limit;
const out = [];
for (let i = 0; i < limit; i += 1) {
out.push(rows[Math.floor(i * step)]);
}
return out;
}
async function loadCurrentObservation() {
const task = state.task;
const observationId = Number(els.observationSelect.value);
els.loadButton.disabled = true;
els.status.classList.add("loading");
setStatus("Loading parquet files from Hugging Face...");
try {
const samples = await loadSamples(task, observationId);
if (!samples) {
setStatus("Load cancelled.");
return;
}
state.samples = samples;
state.trueTheta = await loadTrueTheta(task, observationId);
state.loadedObservation = observationId;
setStatus(`Loaded ${samples.length.toLocaleString()} samples for observation ${observationId}.`);
drawPlot();
} catch (error) {
state.samples = [];
state.trueTheta = null;
setStatus(error && error.message ? error.message : String(error));
clearCanvas("Could not load samples.");
} finally {
els.loadButton.disabled = false;
els.status.classList.remove("loading");
}
}
function clearCanvas(message) {
state.canvasMessage = message;
const canvas = els.canvas;
// Undo any explicit sizing a previous large pairplot applied.
canvas.style.width = "100%";
canvas.style.height = "auto";
const ctx = canvas.getContext("2d");
const ratio = window.devicePixelRatio || 1;
const width = Math.max(320, Math.floor(canvas.clientWidth || 900));
const height = Math.min(900, Math.max(320, width));
canvas.width = Math.floor(width * ratio);
canvas.height = Math.floor(height * ratio);
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = theme().canvasBg;
ctx.fillRect(0, 0, width, height);
ctx.fillStyle = theme().text;
ctx.textAlign = "center";
ctx.font = "14px system-ui, sans-serif";
ctx.fillText(message, width / 2, height / 2);
}
function extent(values) {
let lo = Infinity;
let hi = -Infinity;
for (const value of values) {
if (!Number.isFinite(value)) continue;
lo = Math.min(lo, value);
hi = Math.max(hi, value);
}
if (!Number.isFinite(lo) || !Number.isFinite(hi)) return [-1, 1];
if (lo === hi) return [lo - 1, hi + 1];
const pad = (hi - lo) * 0.05;
return [lo - pad, hi + pad];
}
function scale(value, domain, range) {
return range[0] + ((value - domain[0]) / (domain[1] - domain[0])) * (range[1] - range[0]);
}
function drawHistogram(ctx, values, x0, y0, size, domain) {
const bins = 28;
const counts = Array.from({ length: bins }, () => 0);
for (const value of values) {
const idx = Math.max(0, Math.min(bins - 1, Math.floor(((value - domain[0]) / (domain[1] - domain[0])) * bins)));
counts[idx] += 1;
}
const maxCount = Math.max(...counts, 1);
ctx.fillStyle = theme().hist;
counts.forEach((count, idx) => {
const h = (count / maxCount) * (size - 22);
const x = x0 + 10 + (idx / bins) * (size - 20);
const w = (size - 20) / bins - 1;
ctx.fillRect(x, y0 + size - 10 - h, w, h);
});
}
function drawPlot() {
if (!state.samples.length) {
clearCanvas("Click Load to fetch posterior samples.");
return;
}
const dims = selectedDims();
if (dims.length < 2) {
clearCanvas("Select at least two dimensions.");
return;
}
const limit = Number(els.pointLimitSelect.value);
const rows = downsampleRows(state.samples, limit);
const canvas = els.canvas;
const ctx = canvas.getContext("2d");
const ratio = window.devicePixelRatio || 1;
const n = dims.length;
const margin = 34;
const gap = 7;
// Fit the grid to the container width; once cells would drop below
// MIN_CELL_PX the canvas grows instead and .plot-scroll scrolls.
const available = Math.max(340, Math.floor(canvas.parentElement.clientWidth || 900));
const fitCell = (Math.min(available, 980) - margin * 2 - gap * (n - 1)) / n;
const cell = Math.max(fitCell, MIN_CELL_PX);
const cssSize = Math.ceil(margin * 2 + cell * n + gap * (n - 1));
canvas.style.width = cssSize + "px";
canvas.style.height = cssSize + "px";
canvas.width = Math.floor(cssSize * ratio);
canvas.height = Math.floor(cssSize * ratio);
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
ctx.clearRect(0, 0, cssSize, cssSize);
ctx.fillStyle = theme().canvasBg;
ctx.fillRect(0, 0, cssSize, cssSize);
const domains = new Map();
for (const dim of dims) {
domains.set(dim, extent(state.samples.map((row) => row[dim])));
}
ctx.font = "11px system-ui, sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
dims.forEach((yDim, rowIdx) => {
dims.forEach((xDim, colIdx) => {
const x0 = margin + colIdx * (cell + gap);
const y0 = margin + rowIdx * (cell + gap);
ctx.strokeStyle = theme().grid;
ctx.lineWidth = 1;
ctx.strokeRect(x0, y0, cell, cell);
if (rowIdx === colIdx) {
drawHistogram(ctx, rows.map((row) => row[xDim]), x0, y0, cell, domains.get(xDim));
} else {
ctx.fillStyle = theme().point;
for (const row of rows) {
const x = scale(row[xDim], domains.get(xDim), [x0 + 8, x0 + cell - 8]);
const y = scale(row[yDim], domains.get(yDim), [y0 + cell - 8, y0 + 8]);
ctx.fillRect(x, y, 1.8, 1.8);
}
if (state.trueTheta) {
const tx = scale(state.trueTheta[xDim], domains.get(xDim), [x0 + 8, x0 + cell - 8]);
const ty = scale(state.trueTheta[yDim], domains.get(yDim), [y0 + cell - 8, y0 + 8]);
ctx.strokeStyle = theme().truth;
ctx.lineWidth = 1.8;
ctx.beginPath();
ctx.moveTo(tx - 5, ty);
ctx.lineTo(tx + 5, ty);
ctx.moveTo(tx, ty - 5);
ctx.lineTo(tx, ty + 5);
ctx.stroke();
}
}
if (rowIdx === n - 1) {
ctx.fillStyle = theme().text;
ctx.fillText(`θ${xDim}`, x0 + cell / 2, y0 + cell + 15);
}
if (colIdx === 0) {
ctx.save();
ctx.translate(x0 - 17, y0 + cell / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillStyle = theme().text;
ctx.fillText(`θ${yDim}`, 0, 0);
ctx.restore();
}
});
});
}
async function init() {
const response = await fetch("manifest.json", { cache: "no-cache" });
state.manifest = await response.json();
renderTaskOptions();
const initialTask = taskByName(params.get("task"));
els.taskSelect.value = initialTask.task;
applyTask(initialTask);
const requestedObservation = Number(params.get("observation"));
if (Number.isInteger(requestedObservation) && requestedObservation >= 0 && requestedObservation < initialTask.num_observations) {
els.observationSelect.value = String(requestedObservation);
}
els.taskSelect.addEventListener("change", () => applyTask(taskByName(els.taskSelect.value)));
els.loadButton.addEventListener("click", loadCurrentObservation);
els.pointLimitSelect.addEventListener("change", drawPlot);
window.addEventListener("resize", drawPlot);
if (params.get("autoload") === "1") {
loadCurrentObservation();
}
}
init().catch((error) => {
setStatus(error && error.message ? error.message : String(error));
});