(function () { "use strict"; const CURSOR_COLOR = "#D55E00"; const PLAYBACK_DURATION_MS = 4200; let activeState = null; let scheduleGeneration = 0; function clamp(value, low, high) { return Math.max(low, Math.min(high, value)); } function traceRole(trace) { return trace && trace.meta && trace.meta.benchdash_role; } function traceIndex(graph, role) { return graph.data.findIndex((trace) => traceRole(trace) === role); } function graphDataset(graph) { return graph && graph.layout && graph.layout.meta && graph.layout.meta.benchdash_dataset; } function formatNumber(value, digits) { if (!Number.isFinite(Number(value))) return "—"; const number = Number(value); const magnitude = Math.abs(number); const precision = magnitude >= 10 ? 2 : digits; return number.toFixed(precision).replace(/^-/, "−"); } function formatSignedTime(value) { const rounded = Math.round(Number(value)); if (rounded === 0) return "0 ms"; return `${rounded < 0 ? "−" : "+"}${Math.abs(rounded)} ms`; } function makeElement(tag, className, text) { const node = document.createElement(tag); if (className) node.className = className; if (text !== undefined) node.textContent = text; return node; } function buildControls(state) { const lead = makeElement("div", "dataset-link-lead"); lead.append( makeElement("strong", "dataset-link-title", "Linked trial"), makeElement( "span", "dataset-link-hint", "Playback loops automatically. Hover to pause; scrub to inspect." ) ); const button = makeElement("button", "dataset-link-button", "Play"); button.type = "button"; button.setAttribute("aria-label", "Play linked neural activity and target trajectory"); const slider = makeElement("input", "dataset-link-slider"); slider.type = "range"; slider.min = "0"; slider.max = String(state.payload.time_ms.length - 1); slider.step = "1"; slider.value = String(state.payload.initial_index || 0); slider.setAttribute("aria-label", "Linked trial time bin"); const readout = makeElement("div", "dataset-link-readout"); const time = makeElement("span", "dataset-link-time"); const value = makeElement("span", "dataset-link-value"); readout.append(time, value); const transport = makeElement("div", "dataset-link-transport"); transport.append(button, slider, readout); state.controls.replaceChildren(lead, transport); state.button = button; state.slider = slider; state.timeReadout = time; state.valueReadout = value; } function setButton(state) { state.button.textContent = state.playing ? "Pause" : "Play"; state.button.setAttribute( "aria-label", state.playing ? "Pause linked neural activity and target trajectory" : "Play linked neural activity and target trajectory" ); } function positionPlayhead(state) { if (!state.neural.isConnected || !state.neural._fullLayout) return; const layout = state.neural._fullLayout; const axis = layout.xaxis; const size = layout._size; if (!axis || !size) return; const time = Number(state.payload.time_ms[state.index]); const left = size.l + axis.l2p(time); state.playhead.style.left = `${left}px`; state.playhead.style.top = `${size.t}px`; state.playhead.style.height = `${size.h}px`; } function updateReadout(state) { const index = state.index; const x = Number(state.payload.plot_x[index]); const y = Number(state.payload.plot_y[index]); const neuralTime = Number(state.payload.time_ms[index]); const targetLag = Number(state.payload.target_lag_ms || 0); state.timeReadout.textContent = `Neural time ${formatSignedTime(neuralTime)}`; if (targetLag !== 0) { state.valueReadout.textContent = ( `Target time ${formatSignedTime(neuralTime + targetLag)}` + ` · x ${formatNumber(x, 3)} · y ${formatNumber(y, 3)}` ); } else if (state.payload.value_kind === "force") { state.valueReadout.textContent = `Paired force ${formatNumber(y, 3)}`; } else { state.valueReadout.textContent = ( `Paired target x ${formatNumber(x, 3)} · y ${formatNumber(y, 3)}` ); } } function renderIndex(state, requestedIndex) { const lastIndex = state.payload.time_ms.length - 1; const index = clamp(Math.round(Number(requestedIndex)), 0, lastIndex); if (!Number.isFinite(index)) return; state.index = index; state.slider.value = String(index); positionPlayhead(state); updateReadout(state); if (state.renderFrame !== null) cancelAnimationFrame(state.renderFrame); state.renderFrame = requestAnimationFrame(() => { state.renderFrame = null; if (!state.target.isConnected || !state.target._fullLayout || !window.Plotly) return; const progressX = state.payload.plot_x.slice(0, index + 1); const progressY = state.payload.plot_y.slice(0, index + 1); window.Plotly.restyle( state.target, { x: [progressX, [state.payload.plot_x[index]]], y: [progressY, [state.payload.plot_y[index]]], }, [state.progressTrace, state.cursorTrace] ); }); } function pause(state) { state.playing = false; if (state.animationFrame !== null) cancelAnimationFrame(state.animationFrame); state.animationFrame = null; setButton(state); } function play(state) { pause(state); const finalIndex = state.payload.time_ms.length - 1; if (state.index >= finalIndex) renderIndex(state, 0); const startIndex = state.index; const stepDuration = PLAYBACK_DURATION_MS / Math.max(finalIndex, 1); const start = performance.now(); state.playing = true; setButton(state); const advance = (now) => { if (!state.playing || activeState !== state) return; const elapsedSteps = Math.floor((now - start) / stepDuration); const nextIndex = Math.min(finalIndex, startIndex + elapsedSteps); if (nextIndex !== state.index) renderIndex(state, nextIndex); if (nextIndex >= finalIndex) { state.animationFrame = requestAnimationFrame(() => { if (!state.playing || activeState !== state) return; renderIndex(state, 0); play(state); }); return; } state.animationFrame = requestAnimationFrame(advance); }; state.animationFrame = requestAnimationFrame(advance); } function eventIndex(state, event) { const point = event && event.points && event.points[0]; const custom = point && point.customdata; if (Array.isArray(custom) && Number.isFinite(Number(custom[1]))) { return Number(custom[1]); } const time = point && Number(point.x); if (!Number.isFinite(time)) return null; let bestIndex = 0; let bestDistance = Infinity; state.payload.time_ms.forEach((candidate, index) => { const distance = Math.abs(Number(candidate) - time); if (distance < bestDistance) { bestDistance = distance; bestIndex = index; } }); return bestIndex; } function bindInteractions(state) { const abort = state.abortController; const pauseForHover = (event, fromTarget) => { const point = event && event.points && event.points[0]; if (fromTarget && (!point || point.curveNumber !== state.exampleTrace)) return; const index = eventIndex(state, event); if (index === null) return; if (state.playing) state.resumeAfterHover = true; pause(state); renderIndex(state, index); }; const holdSelection = (event, fromTarget) => { const point = event && event.points && event.points[0]; if (fromTarget && (!point || point.curveNumber !== state.exampleTrace)) return; const index = eventIndex(state, event); if (index === null) return; state.resumeAfterHover = false; pause(state); renderIndex(state, index); }; const neuralHover = (event) => pauseForHover(event, false); const neuralClick = (event) => holdSelection(event, false); const targetHover = (event) => pauseForHover(event, true); const targetClick = (event) => holdSelection(event, true); const resumeAfterHover = () => { if (!state.resumeAfterHover || activeState !== state) return; state.resumeAfterHover = false; play(state); }; state.neural.on("plotly_hover", neuralHover); state.neural.on("plotly_click", neuralClick); state.target.on("plotly_hover", targetHover); state.target.on("plotly_click", targetClick); state.plotlyListeners = [ [state.neural, "plotly_hover", neuralHover], [state.neural, "plotly_click", neuralClick], [state.target, "plotly_hover", targetHover], [state.target, "plotly_click", targetClick], ]; state.neural.addEventListener("mouseleave", resumeAfterHover, { signal: abort.signal }); state.target.addEventListener("mouseleave", resumeAfterHover, { signal: abort.signal }); state.button.addEventListener("click", () => { state.resumeAfterHover = false; if (state.playing) pause(state); else play(state); }, { signal: abort.signal }); state.slider.addEventListener("input", () => { state.resumeAfterHover = false; pause(state); renderIndex(state, Number(state.slider.value)); }, { signal: abort.signal }); state.resizeObserver = new ResizeObserver(() => { requestAnimationFrame(() => positionPlayhead(state)); }); state.resizeObserver.observe(state.neural); } function maybeAutoplay(state) { state.autoplayTimer = window.setTimeout(() => { if (activeState === state) { renderIndex(state, 0); play(state); } }, 150); } function destroy(state) { if (!state) return; pause(state); if (state.renderFrame !== null) cancelAnimationFrame(state.renderFrame); if (state.autoplayTimer !== null) window.clearTimeout(state.autoplayTimer); if (state.resizeObserver) state.resizeObserver.disconnect(); if (state.abortController) state.abortController.abort(); (state.plotlyListeners || []).forEach(([graph, event, listener]) => { if (graph && typeof graph.removeListener === "function") { graph.removeListener(event, listener); } }); if (state.playhead) state.playhead.remove(); } function mount(payload) { const controls = document.getElementById("dataset-link-controls"); const neural = document.querySelector("#dataset-neural-example .js-plotly-plot"); const target = document.querySelector("#dataset-target-trajectory .js-plotly-plot"); if (!controls || !neural || !target || !neural._fullLayout || !target._fullLayout) { return false; } if (graphDataset(neural) !== payload.dataset || graphDataset(target) !== payload.dataset) { return false; } const exampleTrace = traceIndex(target, "example_trajectory"); const progressTrace = traceIndex(target, "linked_target_progress"); const cursorTrace = traceIndex(target, "linked_target_cursor"); if ([exampleTrace, progressTrace, cursorTrace].some((index) => index < 0)) return false; const state = { payload, controls, neural, target, exampleTrace, progressTrace, cursorTrace, index: payload.initial_index || 0, playing: false, resumeAfterHover: false, animationFrame: null, renderFrame: null, autoplayTimer: null, resizeObserver: null, abortController: new AbortController(), plotlyListeners: [], }; const playhead = makeElement("div", "dataset-linked-playhead"); playhead.style.backgroundColor = CURSOR_COLOR; playhead.setAttribute("aria-hidden", "true"); neural.style.position = "relative"; neural.appendChild(playhead); state.playhead = playhead; buildControls(state); bindInteractions(state); activeState = state; renderIndex(state, state.index); maybeAutoplay(state); return true; } function schedule(payload, activeTab) { scheduleGeneration += 1; const generation = scheduleGeneration; destroy(activeState); activeState = null; const controls = document.getElementById("dataset-link-controls"); if (!payload || !payload.enabled || activeTab !== "datasets") { if (controls) controls.replaceChildren(); return `${payload && payload.dataset ? payload.dataset : "none"}:inactive`; } let attempts = 0; const tryMount = () => { if (generation !== scheduleGeneration) return; if (mount(payload)) return; attempts += 1; if (attempts < 180) requestAnimationFrame(tryMount); }; requestAnimationFrame(tryMount); return `${payload.dataset}:scheduled`; } window.benchdashDatasetLink = { schedule }; })();