const version = new URL(import.meta.url).searchParams.get("v") || "dev"; const { makePlot, rankedMethods } = await import(`./explorer-plot.mjs?v=${version}`); const $ = (selector) => document.querySelector(selector); const escapeHtml = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]); const display = (value) => value == null ? "—" : typeof value === "number" ? value.toLocaleString("en-US", { maximumFractionDigits: 4 }) : escapeHtml(String(value).replaceAll("_", " ")); const state = { catalog: null, scope: [], sample: null, selected: new Set(), loading: null, request: null }; let plotly; function loadPlotly() { plotly ||= new Promise((resolve, reject) => { const script = document.createElement("script"); script.src = "/vendor/plotly.min.js"; script.onload = () => resolve(window.Plotly); script.onerror = () => { script.remove(); plotly = null; reject(new Error("Unable to load the chart. Please try again.")); }; document.head.append(script); }); return plotly; } function options(id, values, preferred) { const select = $(`#${id}`); select.innerHTML = values.map((value) => ``).join(""); if (values.map(String).includes(String(preferred))) select.value = preferred; } function updateSelection(target = null) { const fields = ["dataset", "term", "item", "window"]; const current = Object.fromEntries(fields.map((field) => [field, $(`#sample-${field}`).value])); const effect = $("#sample-effect").value; let candidates = state.catalog.samples.filter((entry) => !effect || entry.effects.includes(effect)); options("sample-dataset", [...new Set(candidates.map((entry) => entry.dataset))].sort(), target?.dataset ?? current.dataset); candidates = candidates.filter((entry) => entry.dataset === $("#sample-dataset").value); options("sample-term", ["short", "medium", "long"].filter((term) => candidates.some((entry) => entry.term === term)), target?.term ?? current.term); candidates = candidates.filter((entry) => entry.term === $("#sample-term").value); state.scope = candidates.sort((a, b) => a.item_id.localeCompare(b.item_id, undefined, { numeric: true }) || a.window_id - b.window_id); options("sample-item", [...new Set(candidates.map((entry) => entry.item_id))], target?.item_id ?? current.item); candidates = candidates.filter((entry) => entry.item_id === $("#sample-item").value); options("sample-window", candidates.map((entry) => entry.window_id), target?.window_id ?? current.window); const selected = candidates.find((entry) => String(entry.window_id) === $("#sample-window").value); if (selected) loadSample(selected); else { state.request?.abort(); state.sample = null; $("#sample-metadata").textContent = ""; $("#sample-content").hidden = true; $("#sample-count").textContent = "No examples match this selection."; $("#sample-previous").disabled = $("#sample-next").disabled = true; } } function renderEffects(sample) { const macro = sample.macro; $("#sample-macro").innerHTML = `
Period ${display(macro.seasonality?.period)}Waveform ${display(macro.seasonality?.waveform)}Level Range [${(macro.level_range || []).map(display).join(", ")}]
${(macro.trend || []).map((segment, index) => ``).join("")}
SegmentDirectionShapeDuration
${index + 1}${display(segment.direction)}${display(segment.shape)}${(segment.duration * 100).toFixed(1)}%
`; const columns = ["family", "subtype", "direction", "amplitude_trend", "envelope_change", "start", "end", "intensity"]; $("#sample-micro").innerHTML = sample.micro.length ? sample.micro.map((event, index) => `${index + 1}${columns.map((field) => `${display(event[field])}`).join("")}`).join("") : 'No Micro effects.'; $("#sample-score-body").innerHTML = rankedMethods(sample.forecasts).map((method) => `${escapeHtml(method.name)}${escapeHtml(method.category)}${escapeHtml(method.input)}${method.mase.toFixed(4)}`).join(""); $(".sample-score-shell").scrollTop = 0; } async function renderPlot() { const sample = state.sample; if (!sample) return; const Plotly = await loadPlotly(); if (sample !== state.sample) return; const figure = makePlot(sample, state.selected, $("#history-context").value); await Plotly.react("sample-plot", figure.data, figure.layout, figure.config); } function showError(error) { $("#explorer-error").textContent = error.message; $("#explorer-warning").classList.remove("hidden"); } async function loadSample(entry) { state.request?.abort(); const controller = new AbortController(); state.request = controller; $("#explorer-warning").classList.add("hidden"); const content = $("#sample-content"); // Keep the previous sample in the layout so loading cannot collapse the page. content.setAttribute("aria-busy", "true"); const index = state.scope.findIndex((sample) => sample.id === entry.id); $("#sample-count").textContent = `${index + 1} / ${state.scope.length.toLocaleString()} examples`; $("#sample-previous").disabled = index === 0; $("#sample-next").disabled = index === state.scope.length - 1; if (content.hidden) $("#sample-metadata").textContent = "Loading example…"; try { const response = await fetch(`/api/explorer/sample/${entry.id}`, { signal: controller.signal }); if (!response.ok) throw new Error("This example is currently unavailable. Please try again."); const sample = await response.json(); if (controller.signal.aborted) return; if (state.sample?.id !== sample.id) { const best = rankedMethods(sample.forecasts)[0]; state.selected = new Set(best ? [best.id] : []); } state.sample = sample; $("#sample-content").hidden = false; $("#sample-metadata").textContent = `${sample.dataset} · ${sample.term} · item ${sample.item_id} · window ${sample.window_id} · frequency ${sample.frequency} · ${sample.n_history.toLocaleString()} history points · ${sample.n_horizon.toLocaleString()} future points`; renderEffects(sample); await renderPlot(); } catch (error) { if (error.name !== "AbortError" && !controller.signal.aborted) { if (content.hidden) $("#sample-metadata").textContent = "Example unavailable"; showError(error); } } finally { // An aborted request must not clear the next request's loading state. if (state.request === controller) content.removeAttribute("aria-busy"); } } async function initialize() { $("#explorer-warning").classList.add("hidden"); $("#sample-count").textContent = "Loading examples…"; try { const response = await fetch("/api/explorer/catalog"); if (!response.ok) throw new Error("Examples are currently unavailable. Please try again later."); state.catalog = await response.json(); const effects = [...new Set(state.catalog.samples.flatMap((entry) => entry.effects))].sort(); $("#sample-effect").innerHTML = '' + effects.map((effect) => ``).join(""); $("#explorer-filters").disabled = false; updateSelection(); } catch (error) { state.loading = null; $("#sample-count").textContent = "Examples unavailable"; showError(error); } } export async function activateExplorer() { if (!state.catalog) { state.loading ||= initialize(); await state.loading; } else if (state.sample && window.Plotly) { window.Plotly.Plots.resize($("#sample-plot")); } } for (const field of ["dataset", "term", "item", "window", "effect"]) $(`#sample-${field}`).addEventListener("change", () => updateSelection()); for (const direction of ["previous", "next"]) $(`#sample-${direction}`).addEventListener("click", () => { const index = state.scope.findIndex((entry) => entry.dataset === $("#sample-dataset").value && entry.term === $("#sample-term").value && entry.item_id === $("#sample-item").value && String(entry.window_id) === $("#sample-window").value); const entry = state.scope[index + (direction === "previous" ? -1 : 1)]; if (entry) updateSelection(entry); }); $("#sample-score-body").addEventListener("change", (event) => { const id = event.target.dataset.method; if (!id) return; if (event.target.checked) state.selected.add(id); else state.selected.delete(id); renderPlot().catch(showError); }); $("#history-context").addEventListener("change", () => renderPlot().catch(showError)); $("#explorer-retry").addEventListener("click", () => { if (state.catalog) updateSelection(); else activateExplorer(); });