import { useRef, useEffect, useState, useCallback } from 'react'; import { Maximize2, Minimize2 } from 'lucide-react'; import type { WaveformData } from '../../../lib/types'; import { formatAxisValue, TRACE_COLORS } from '../../../lib/waveform-utils'; import { cn } from '../../../lib/cn'; import { useFullscreen } from '../../../lib/useFullscreen'; import { ScopeWaveformViewer } from './ScopeWaveformViewer'; // uPlot is a vanilla JS lib; import it and its CSS import uPlot from 'uplot'; import 'uplot/dist/uPlot.min.css'; interface WaveformViewerProps { waveform: WaveformData; className?: string; } function getWfColors() { const style = getComputedStyle(document.documentElement); return { axis: style.getPropertyValue('--color-wf-axis').trim() || '#475569', grid: style.getPropertyValue('--color-wf-grid').trim() || 'rgba(51, 65, 85, 0.5)', tick: style.getPropertyValue('--color-wf-tick').trim() || '#334155', }; } function buildOpts( waveform: WaveformData, width: number, height: number, xType: string, yLabel: string, ): uPlot.Options { const traceNames = Object.keys(waveform.y_data); const wfColors = getWfColors(); const series: uPlot.Series[] = [ { label: xType === 'frequency' ? 'Frequency' : 'Time' }, ...traceNames.map((name, i) => ({ label: name, stroke: TRACE_COLORS[i % TRACE_COLORS.length], width: 2, })), ]; const axes: uPlot.Axis[] = [ { stroke: wfColors.axis, grid: { stroke: wfColors.grid, width: 1 }, ticks: { stroke: wfColors.tick, width: 1 }, font: '11px system-ui, sans-serif', values: (_u: uPlot, vals: number[]) => vals.map((v) => formatAxisValue(v, xType)), }, { stroke: wfColors.axis, grid: { stroke: wfColors.grid, width: 1 }, ticks: { stroke: wfColors.tick, width: 1 }, font: '11px system-ui, sans-serif', values: (_u: uPlot, vals: number[]) => vals.map((v) => formatAxisValue(v, yLabel)), }, ]; return { width, height, series, axes, scales: { x: xType === 'frequency' ? { distr: 3 } : {}, }, cursor: { drag: { x: true, y: true, setScale: true }, }, legend: { show: true, }, }; } function TransientPlot({ waveform, width, height, }: { waveform: WaveformData; width: number; height?: number; }) { const plotRef = useRef(null); const uplotRef = useRef(null); useEffect(() => { if (!plotRef.current || width <= 0) return; const traceNames = Object.keys(waveform.y_data); const xData = new Float64Array(waveform.x_data); const data: uPlot.AlignedData = [ xData as unknown as number[], ...traceNames.map( (name) => new Float64Array(waveform.y_data[name]) as unknown as number[], ), ]; // Determine y-axis type from variable types const yType = waveform.variables.length > 1 ? waveform.variables[1].type : 'voltage'; const opts = buildOpts(waveform, width, height ?? 280, waveform.x_type, yType); const u = new uPlot(opts, data, plotRef.current); uplotRef.current = u; return () => { u.destroy(); uplotRef.current = null; }; }, [waveform, width, height]); return
; } function ACPlot({ waveform, width, height, }: { waveform: WaveformData; width: number; height?: number; }) { const magRef = useRef(null); const phaseRef = useRef(null); const magPlotRef = useRef(null); const phasePlotRef = useRef(null); useEffect(() => { if (!magRef.current || !phaseRef.current || width <= 0) return; const xData = new Float64Array(waveform.x_data); const magData = waveform.y_magnitude_db || {}; const phaseData = waveform.y_phase_deg || {}; const magTraces = Object.keys(magData); const phaseTraces = Object.keys(phaseData); // Magnitude plot if (magTraces.length > 0) { const magSeries: uPlot.AlignedData = [ xData as unknown as number[], ...magTraces.map( (name) => new Float64Array(magData[name]) as unknown as number[], ), ]; const magH = height ? Math.round(height * 0.55) : 220; const magOpts = buildOpts(waveform, width, magH, 'frequency', 'dB'); magOpts.series = [ { label: 'Frequency' }, ...magTraces.map((name, i) => ({ label: `|${name}| (dB)`, stroke: TRACE_COLORS[i % TRACE_COLORS.length], width: 2, })), ]; const u1 = new uPlot(magOpts, magSeries, magRef.current); magPlotRef.current = u1; } // Phase plot if (phaseTraces.length > 0) { const phaseSeries: uPlot.AlignedData = [ xData as unknown as number[], ...phaseTraces.map( (name) => new Float64Array(phaseData[name]) as unknown as number[], ), ]; const phaseH = height ? height - Math.round(height * 0.55) : 180; const phaseOpts = buildOpts( waveform, width, phaseH, 'frequency', 'degrees', ); phaseOpts.series = [ { label: 'Frequency' }, ...phaseTraces.map((name, i) => ({ label: `Phase(${name})`, stroke: TRACE_COLORS[i % TRACE_COLORS.length], width: 2, dash: [6, 3], })), ]; const u2 = new uPlot(phaseOpts, phaseSeries, phaseRef.current); phasePlotRef.current = u2; } return () => { magPlotRef.current?.destroy(); phasePlotRef.current?.destroy(); magPlotRef.current = null; phasePlotRef.current = null; }; }, [waveform, width, height]); return (
); } /* Scope mode toggle icon — inline SVG of a simple oscilloscope shape */ function ScopeToggleIcon({ active }: { active: boolean }) { return ( ); } export function WaveformViewer({ waveform, className }: WaveformViewerProps) { const containerRef = useRef(null); const [width, setWidth] = useState(0); const [height, setHeight] = useState(0); const { isFullscreen, toggleFullscreen } = useFullscreen(); const [scopeMode, setScopeMode] = useState(() => { try { return localStorage.getItem('spicebook-scope-mode') === 'true'; } catch { return false; } }); const updateDimensions = useCallback(() => { if (containerRef.current) { setWidth(containerRef.current.clientWidth); setHeight(containerRef.current.clientHeight); } }, []); useEffect(() => { updateDimensions(); const observer = new ResizeObserver(updateDimensions); if (containerRef.current) { observer.observe(containerRef.current); } return () => observer.disconnect(); }, [updateDimensions]); const toggleScope = useCallback(() => { setScopeMode(prev => { const next = !prev; try { localStorage.setItem('spicebook-scope-mode', String(next)); } catch { /* noop */ } return next; }); }, []); const traceCount = Object.keys(waveform.y_data).length; const isEmpty = traceCount === 0 && !waveform.y_magnitude_db && !waveform.y_phase_deg; // In fullscreen, subtract toolbar + padding from available height const plotWidth = isFullscreen ? Math.max(width - 32, 100) : width; const plotHeight = isFullscreen ? Math.max(height - 80, 200) : undefined; return (
{/* Control buttons */} {!isEmpty && (
)} {/* Plot area */}
{isEmpty ? (
No waveform data to display.
) : scopeMode ? ( ) : waveform.is_complex ? ( ) : ( )}
); }