// Themed Plotly rendering. Figures come from the backend as plain // {data, layout} JSON; we overlay transparent backgrounds and the current // theme's font/grid colors, and re-render live plots on theme switch. // // IMPORTANT (inherited from miru-tracer): never give a rendered figure a // fixed width inside a scrollable container — Plotly's responsive resize + // a toggling scrollbar forms a ResizeObserver feedback loop that freezes // the browser. Figures stay autosized; wide data pans via the figure's own // dragmode. const live = new Map(); // element -> figure JSON function themeColors() { const styles = getComputedStyle(document.documentElement); return { text: styles.getPropertyValue('--text').trim() || '#e6e9f0', muted: styles.getPropertyValue('--text-muted').trim() || '#8b93a7', grid: styles.getPropertyValue('--plot-grid').trim() || 'rgba(127,127,127,0.18)', font: styles.getPropertyValue('--font-ui').trim() || 'Inter, sans-serif', }; } function themedLayout(layout = {}) { const c = themeColors(); const themed = structuredClone(layout); themed.paper_bgcolor = 'rgba(0,0,0,0)'; themed.plot_bgcolor = 'rgba(0,0,0,0)'; themed.font = { ...(themed.font || {}), color: c.text, family: c.font }; themed.autosize = true; delete themed.width; // never fixed-width (see module note) for (const key of Object.keys(themed)) { if (key.startsWith('xaxis') || key.startsWith('yaxis')) { themed[key] = { ...themed[key], gridcolor: c.grid, zerolinecolor: c.grid, linecolor: c.grid, tickfont: { ...(themed[key]?.tickfont || {}), color: c.muted }, }; } } if (themed.legend) themed.legend = { ...themed.legend, font: { color: c.muted } }; return themed; } export function renderPlot(target, fig) { if (!fig || !fig.data) { clearPlot(target); return; } live.set(target, fig); window.Plotly.react(target, fig.data, themedLayout(fig.layout), { responsive: true, displaylogo: false, modeBarButtonsToRemove: ['select2d', 'lasso2d'], }); target.classList.add('has-plot'); } export function clearPlot(target) { live.delete(target); if (target.classList.contains('has-plot')) { window.Plotly.purge(target); target.classList.remove('has-plot'); } target.replaceChildren(); } export function rethemePlots() { for (const [target, fig] of live) { if (target.isConnected) renderPlot(target, fig); else live.delete(target); } }