From 3c9c83742d3683cc1e04895b5ce57fa8185cdd76 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Mon, 16 Feb 2026 23:27:46 -0700 Subject: [PATCH] Fix scope viewer: initialize div state from data range to prevent empty CRT xDiv/yDiv were initialized as null and excluded from the main uPlot effect's dependency array, causing the effect to bail on its null guard and never re-run when the init effect set them to computed values. --- .../notebook/output/ScopeWaveformViewer.tsx | 35 +++++++++---------- frontend/src/styles/scope-skin.css | 4 ++- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/notebook/output/ScopeWaveformViewer.tsx b/frontend/src/components/notebook/output/ScopeWaveformViewer.tsx index 4cb9642..5e9b2e9 100644 --- a/frontend/src/components/notebook/output/ScopeWaveformViewer.tsx +++ b/frontend/src/components/notebook/output/ScopeWaveformViewer.tsx @@ -104,18 +104,8 @@ export function ScopeWaveformViewer({ waveform, width, onExitScope }: ScopeWavef return waveform.variables.length > 1 ? waveform.variables[1].type : 'voltage'; }, [waveform, isAC]); - // State - const [activeTrace, setActiveTrace] = useState(0); - const [xDiv, setXDiv] = useState(null); - const [yDiv, setYDiv] = useState(null); - const [skinId, setSkinId] = useState(() => { - const stored = safeGetItem('spicebook-scope-skin') as SkinId | null; - return stored && SKINS[stored] ? stored : '465'; - }); - - const skin = SKINS[skinId]; - // Compute data range using loop-based min/max (safe for large arrays) + // Must be computed before useState so div initializers can reference them const { xMin, xMax, yMin, yMax } = useMemo(() => { const [xLo, xHi] = arrayMinMax(waveform.x_data); const yData = isAC && waveform.y_magnitude_db @@ -125,7 +115,18 @@ export function ScopeWaveformViewer({ waveform, width, onExitScope }: ScopeWavef return { xMin: xLo, xMax: xHi, yMin: yLo, yMax: yHi }; }, [waveform, isAC]); - // Initialize div values from data range + // State — xDiv/yDiv initialized from data range (never null) + const [activeTrace, setActiveTrace] = useState(0); + const [xDiv, setXDiv] = useState(() => autoDiv(xMin, xMax, NUM_H_DIVS, xSeqForType(xType))); + const [yDiv, setYDiv] = useState(() => autoDiv(yMin, yMax, NUM_V_DIVS, ySeqForType(yType))); + const [skinId, setSkinId] = useState(() => { + const stored = safeGetItem('spicebook-scope-skin') as SkinId | null; + return stored && SKINS[stored] ? stored : '465'; + }); + + const skin = SKINS[skinId]; + + // Re-initialize div values when data range changes (new waveform loaded) useEffect(() => { setXDiv(autoDiv(xMin, xMax, NUM_H_DIVS, xSeqForType(xType))); setYDiv(autoDiv(yMin, yMax, NUM_V_DIVS, ySeqForType(yType))); @@ -133,7 +134,7 @@ export function ScopeWaveformViewer({ waveform, width, onExitScope }: ScopeWavef // Build and mount uPlot (only on data/size changes, NOT on trace/div changes) useEffect(() => { - if (!screenRef.current || width <= 0 || xDiv === null || yDiv === null) return; + if (!screenRef.current || width <= 0) return; const el = screenRef.current; const rect = el.getBoundingClientRect(); @@ -219,7 +220,7 @@ export function ScopeWaveformViewer({ waveform, width, onExitScope }: ScopeWavef // Update X/Y scale via setScale (no chart rebuild) useEffect(() => { const u = uplotRef.current; - if (!u || xDiv === null || yDiv === null) return; + if (!u) return; const xMid = (xMin + xMax) / 2; const yMid = (yMin + yMax) / 2; @@ -240,7 +241,6 @@ export function ScopeWaveformViewer({ waveform, width, onExitScope }: ScopeWavef const stepXDiv = useCallback(() => { setXDiv(prev => { - if (prev === null) return prev; const seq = xSeqForType(xType); const next = step125(prev, seq, 1); if (next === prev) return seq[0]; @@ -250,7 +250,6 @@ export function ScopeWaveformViewer({ waveform, width, onExitScope }: ScopeWavef const stepYDiv = useCallback(() => { setYDiv(prev => { - if (prev === null) return prev; const seq = ySeqForType(yType); const next = step125(prev, seq, 1); if (next === prev) return seq[0]; @@ -273,8 +272,8 @@ export function ScopeWaveformViewer({ waveform, width, onExitScope }: ScopeWavef : 0; const currentTraceName = traceNames[activeTrace] || '\u2014'; - const xDivLabel = xDiv !== null ? formatEng(xDiv, divUnit(xType) + '/div') : '\u2014'; - const yDivLabel = yDiv !== null ? formatEng(yDiv, divUnit(yType) + '/div') : '\u2014'; + const xDivLabel = formatEng(xDiv, divUnit(xType) + '/div'); + const yDivLabel = formatEng(yDiv, divUnit(yType) + '/div'); return (
diff --git a/frontend/src/styles/scope-skin.css b/frontend/src/styles/scope-skin.css index 87eefbf..21b1f09 100644 --- a/frontend/src/styles/scope-skin.css +++ b/frontend/src/styles/scope-skin.css @@ -125,8 +125,10 @@ /* uPlot chart fills the CRT screen */ .scope-skin-active .scope-screen .uplot { - position: absolute; + position: absolute !important; inset: 0; + width: 100% !important; + height: 100% !important; } /* ── Graticule overlay (10 horizontal × 8 vertical) ──────── */