spectraview 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +7 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +63 -1
- package/dist/index.d.ts +63 -1
- package/dist/index.js +7 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/components/SpectraView/SpectraView.tsx","../src/utils/scales.ts","../src/utils/colors.ts","../src/hooks/useZoomPan.ts","../src/components/SpectrumCanvas/SpectrumCanvas.tsx","../src/utils/lttb.ts","../src/utils/rendering.ts","../src/components/AxisLayer/AxisLayer.tsx","../src/components/PeakMarkers/PeakMarkers.tsx","../src/components/RegionSelector/RegionSelector.tsx","../src/components/Crosshair/Crosshair.tsx","../src/components/AnnotationLayer/AnnotationLayer.tsx","../src/utils/snap.ts","../src/components/Toolbar/Toolbar.tsx","../src/components/Legend/Legend.tsx","../src/components/DropZone/DropZone.tsx","../src/components/StackedView/StackedView.tsx","../src/hooks/useRegionSelect.ts","../src/hooks/useResizeObserver.ts","../src/hooks/useKeyboardNavigation.ts","../src/utils/a11y.ts","../src/components/Minimap/Minimap.tsx","../src/components/Tooltip/Tooltip.tsx","../src/hooks/usePeakPicking.ts","../src/utils/peaks.ts","../src/hooks/useSpectrumData.ts","../src/parsers/csv.ts","../src/parsers/json.ts","../src/parsers/jcamp.ts","../src/hooks/useExport.ts","../src/utils/svg-export.ts","../src/components/ExportMenu/ExportMenu.tsx","../src/utils/processing.ts","../src/utils/comparison.ts","../src/parsers/spc.ts"],"sourcesContent":["/**\n * SpectraView — Interactive React component for vibrational spectroscopy.\n *\n * @example\n * ```tsx\n * import { SpectraView, parseCsv } from \"spectraview\";\n *\n * const spectrum = parseCsv(csvText);\n * <SpectraView spectra={[spectrum]} reverseX />\n * ```\n *\n * @packageDocumentation\n */\n\n// Main component\nexport { SpectraView } from \"./components/SpectraView/SpectraView\";\n\n// Sub-components (for advanced composition)\nexport { SpectrumCanvas } from \"./components/SpectrumCanvas/SpectrumCanvas\";\nexport { AxisLayer } from \"./components/AxisLayer/AxisLayer\";\nexport { PeakMarkers } from \"./components/PeakMarkers/PeakMarkers\";\nexport { RegionSelector } from \"./components/RegionSelector/RegionSelector\";\nexport { Crosshair } from \"./components/Crosshair/Crosshair\";\nexport { Toolbar } from \"./components/Toolbar/Toolbar\";\nexport { Legend } from \"./components/Legend/Legend\";\nexport { DropZone } from \"./components/DropZone/DropZone\";\nexport { AnnotationLayer } from \"./components/AnnotationLayer/AnnotationLayer\";\nexport { Minimap } from \"./components/Minimap/Minimap\";\nexport { Tooltip } from \"./components/Tooltip/Tooltip\";\n\n// Hooks\nexport { useZoomPan } from \"./hooks/useZoomPan\";\nexport { usePeakPicking } from \"./hooks/usePeakPicking\";\nexport { useSpectrumData } from \"./hooks/useSpectrumData\";\nexport { useExport } from \"./hooks/useExport\";\nexport { useRegionSelect } from \"./hooks/useRegionSelect\";\nexport { useResizeObserver } from \"./hooks/useResizeObserver\";\nexport { useKeyboardNavigation } from \"./hooks/useKeyboardNavigation\";\nexport { StackedView } from \"./components/StackedView/StackedView\";\nexport { ExportMenu } from \"./components/ExportMenu/ExportMenu\";\nexport { generateSvg, downloadSvg, LINE_DASH_PATTERNS } from \"./utils/svg-export\";\nexport {\n prefersReducedMotion,\n generateChartDescription,\n KEYBOARD_SHORTCUTS,\n} from \"./utils/a11y\";\nexport { binarySearchClosest, snapToNearestSpectrum } from \"./utils/snap\";\nexport { lttbDownsample } from \"./utils/lttb\";\nexport {\n baselineRubberBand,\n normalizeMinMax,\n normalizeArea,\n normalizeSNV,\n smoothSavitzkyGolay,\n derivative1st,\n derivative2nd,\n} from \"./utils/processing\";\nexport {\n differenceSpectrum,\n addSpectra,\n scaleSpectrum,\n correlationCoefficient,\n residualSpectrum,\n interpolateToGrid,\n} from \"./utils/comparison\";\n\n// Parsers\nexport { parseJcamp } from \"./parsers/jcamp\";\nexport { parseCsv, parseCsvMulti } from \"./parsers/csv\";\nexport { parseJson } from \"./parsers/json\";\nexport { parseSpc } from \"./parsers/spc\";\n\n// Utilities\nexport { detectPeaks } from \"./utils/peaks\";\nexport {\n computeXExtent,\n computeYExtent,\n createXScale,\n createYScale,\n} from \"./utils/scales\";\nexport {\n SPECTRUM_COLORS,\n LIGHT_THEME,\n DARK_THEME,\n getSpectrumColor,\n getThemeColors,\n} from \"./utils/colors\";\n\n// Types (re-export all)\nexport type {\n Spectrum,\n SpectrumType,\n Peak,\n Region,\n Annotation,\n ViewState,\n Theme,\n DisplayMode,\n Margin,\n LineStyle,\n LegendPosition,\n SpectraViewProps,\n ResolvedConfig,\n} from \"./types\";\n\nexport type { CsvParseOptions } from \"./parsers/csv\";\nexport type { PeakDetectionOptions } from \"./utils/peaks\";\nexport type {\n UseZoomPanOptions,\n UseZoomPanReturn,\n ZoomPanState,\n} from \"./hooks/useZoomPan\";\nexport type { UsePeakPickingOptions } from \"./hooks/usePeakPicking\";\nexport type { UseSpectrumDataReturn } from \"./hooks/useSpectrumData\";\nexport type { UseExportReturn } from \"./hooks/useExport\";\nexport type { CrosshairPosition, CrosshairProps } from \"./components/Crosshair/Crosshair\";\nexport type { LegendProps } from \"./components/Legend/Legend\";\nexport type { DropZoneProps } from \"./components/DropZone/DropZone\";\nexport type { AnnotationLayerProps } from \"./components/AnnotationLayer/AnnotationLayer\";\nexport type { MinimapProps } from \"./components/Minimap/Minimap\";\nexport type { TooltipProps, TooltipData } from \"./components/Tooltip/Tooltip\";\nexport type { SnapResult } from \"./utils/snap\";\nexport type { LTTBPoint } from \"./utils/lttb\";\nexport type { SnapPoint } from \"./components/Crosshair/Crosshair\";\nexport type { UseRegionSelectOptions, UseRegionSelectReturn } from \"./hooks/useRegionSelect\";\nexport type { UseKeyboardNavigationOptions } from \"./hooks/useKeyboardNavigation\";\nexport type { ExportMenuProps } from \"./components/ExportMenu/ExportMenu\";\nexport type { SvgExportOptions } from \"./utils/svg-export\";\n","/**\n * SpectraView — Main interactive spectrum viewer component.\n *\n * Composes the Canvas data layer, SVG axis/annotation layer,\n * crosshair, peak markers, region selection, toolbar, and zoom/pan\n * into a single embeddable React component.\n *\n * Architecture:\n * - Canvas layer: high-performance spectral line rendering (10K+ points)\n * - SVG layer: axes, grid, annotations, crosshair (lightweight interactive elements)\n * - d3-zoom: zoom/pan math via useZoomPan hook\n */\n\nimport { useCallback, useId, useMemo, useRef, useState } from \"react\";\nimport type {\n SpectraViewProps,\n ResolvedConfig,\n Margin,\n Spectrum,\n} from \"../../types\";\nimport { computeXExtent, computeYExtent, createXScale, createYScale } from \"../../utils/scales\";\nimport { getThemeColors } from \"../../utils/colors\";\nimport { useZoomPan } from \"../../hooks/useZoomPan\";\nimport { SpectrumCanvas } from \"../SpectrumCanvas/SpectrumCanvas\";\nimport { AxisLayer } from \"../AxisLayer/AxisLayer\";\nimport { PeakMarkers } from \"../PeakMarkers/PeakMarkers\";\nimport { RegionSelector } from \"../RegionSelector/RegionSelector\";\nimport { Crosshair } from \"../Crosshair/Crosshair\";\nimport type { CrosshairPosition, SnapPoint } from \"../Crosshair/Crosshair\";\nimport { AnnotationLayer } from \"../AnnotationLayer/AnnotationLayer\";\nimport { snapToNearestSpectrum } from \"../../utils/snap\";\nimport { getSpectrumColor } from \"../../utils/colors\";\nimport { Toolbar } from \"../Toolbar/Toolbar\";\nimport { Legend } from \"../Legend/Legend\";\nimport { DropZone } from \"../DropZone/DropZone\";\nimport { StackedView } from \"../StackedView/StackedView\";\nimport { useRegionSelect } from \"../../hooks/useRegionSelect\";\nimport { useResizeObserver } from \"../../hooks/useResizeObserver\";\nimport { useKeyboardNavigation } from \"../../hooks/useKeyboardNavigation\";\nimport { generateChartDescription } from \"../../utils/a11y\";\n\n/** Default chart margins. */\nconst DEFAULT_MARGIN: Margin = {\n top: 20,\n right: 20,\n bottom: 50,\n left: 65,\n};\n\n/** Default component width. */\nconst DEFAULT_WIDTH = 800;\n\n/** Default component height. */\nconst DEFAULT_HEIGHT = 400;\n\n/**\n * Resolve user props into a complete configuration with defaults.\n */\nfunction resolveConfig(props: SpectraViewProps): ResolvedConfig {\n return {\n width: props.width ?? DEFAULT_WIDTH,\n height: props.height ?? DEFAULT_HEIGHT,\n reverseX: props.reverseX ?? false,\n showGrid: props.showGrid ?? true,\n showCrosshair: props.showCrosshair ?? true,\n showToolbar: props.showToolbar ?? true,\n showLegend: props.showLegend ?? true,\n legendPosition: props.legendPosition ?? \"bottom\",\n displayMode: props.displayMode ?? \"overlay\",\n margin: { ...DEFAULT_MARGIN, ...props.margin },\n theme: props.theme ?? \"light\",\n responsive: props.responsive ?? false,\n enableDragDrop: props.enableDragDrop ?? false,\n enableRegionSelect: props.enableRegionSelect ?? false,\n };\n}\n\n/**\n * Infer axis labels from spectrum metadata if not provided.\n */\nfunction inferLabels(\n spectra: Spectrum[],\n xLabel?: string,\n yLabel?: string,\n): { xLabel: string; yLabel: string } {\n const first = spectra[0];\n return {\n xLabel: xLabel ?? first?.xUnit ?? \"x\",\n yLabel: yLabel ?? first?.yUnit ?? \"y\",\n };\n}\n\nexport function SpectraView(props: SpectraViewProps) {\n const {\n spectra,\n peaks = [],\n regions = [],\n annotations = [],\n onPeakClick,\n onViewChange,\n onCrosshairMove,\n onToggleVisibility,\n onFileDrop,\n onRegionSelect,\n canvasRef,\n snapCrosshair = true,\n } = props;\n\n // Responsive sizing\n const { ref: resizeRef, size: measuredSize } = useResizeObserver();\n\n // Unique ID for this instance to avoid clipPath collisions (BUG-1 fix)\n const instanceId = useId();\n const clipId = `spectraview-clip-${instanceId.replace(/:/g, \"\")}`;\n\n const config = useMemo(() => resolveConfig(props), [\n props.width,\n props.height,\n props.reverseX,\n props.showGrid,\n props.showCrosshair,\n props.showToolbar,\n props.showLegend,\n props.legendPosition,\n props.displayMode,\n props.margin,\n props.theme,\n props.responsive,\n props.enableDragDrop,\n props.enableRegionSelect,\n ]);\n\n // Use measured width when responsive, fall back to configured width\n const width =\n config.responsive && measuredSize ? measuredSize.width : config.width;\n const { height, margin, reverseX, theme } = config;\n const plotWidth = width - margin.left - margin.right;\n const plotHeight = height - margin.top - margin.bottom;\n const colors = useMemo(() => getThemeColors(theme), [theme]);\n const labels = useMemo(\n () => inferLabels(spectra, props.xLabel, props.yLabel),\n [spectra, props.xLabel, props.yLabel],\n );\n\n // Compute data extents\n const xExtent = useMemo(() => computeXExtent(spectra), [spectra]);\n const yExtent = useMemo(() => computeYExtent(spectra), [spectra]);\n\n // Create base (unzoomed) scales\n const baseXScale = useMemo(\n () => createXScale(xExtent, width, margin, reverseX),\n [xExtent, width, margin, reverseX],\n );\n const baseYScale = useMemo(\n () => createYScale(yExtent, height, margin),\n [yExtent, height, margin],\n );\n\n // Stable onViewChange wrapper via ref to avoid re-attaching zoom\n const onViewChangeRef = useRef(onViewChange);\n onViewChangeRef.current = onViewChange;\n const stableOnViewChange = useMemo(\n () =>\n (xDomain: [number, number], yDomain: [number, number]) => {\n onViewChangeRef.current?.({ xDomain, yDomain });\n },\n [],\n );\n\n // Zoom/pan behavior\n const {\n zoomRef,\n state: zoomState,\n zoomedXScale,\n zoomedYScale,\n resetZoom,\n zoomIn,\n zoomOut,\n } = useZoomPan({\n plotWidth,\n plotHeight,\n xScale: baseXScale,\n yScale: baseYScale,\n onViewChange: onViewChange ? stableOnViewChange : undefined,\n });\n\n // Region selection (Shift+drag)\n const {\n pendingRegion,\n handleMouseDown: regionMouseDown,\n handleMouseMove: regionMouseMove,\n handleMouseUp: regionMouseUp,\n } = useRegionSelect({\n enabled: config.enableRegionSelect,\n xScale: zoomedXScale,\n onRegionSelect,\n });\n\n // Highlighted spectrum for legend hover\n const [highlightedId, setHighlightedId] = useState<string | null>(null);\n\n // Crosshair state — managed here so the zoom rect handles all mouse events (BUG-2 fix)\n const [crosshairPos, setCrosshairPos] = useState<CrosshairPosition | null>(null);\n const [snapPointState, setSnapPointState] = useState<SnapPoint | null>(null);\n const onCrosshairMoveRef = useRef(onCrosshairMove);\n onCrosshairMoveRef.current = onCrosshairMove;\n\n const handleMouseMove = useCallback(\n (event: React.MouseEvent<SVGRectElement>) => {\n if (!config.showCrosshair) return;\n const rect = event.currentTarget.getBoundingClientRect();\n const px = event.clientX - rect.left;\n const py = event.clientY - rect.top;\n const dataX = zoomedXScale.invert(px);\n const dataY = zoomedYScale.invert(py);\n setCrosshairPos({ px, py, dataX, dataY });\n\n // Snap to nearest spectrum data point\n if (snapCrosshair && spectra.length > 0) {\n const snap = snapToNearestSpectrum(\n spectra,\n dataX,\n py,\n zoomedXScale,\n zoomedYScale,\n );\n if (snap && snap.distance < 50) {\n const spIdx = spectra.findIndex((s) => s.id === snap.spectrumId);\n setSnapPointState({\n px: zoomedXScale(snap.x),\n py: zoomedYScale(snap.y),\n dataX: snap.x,\n dataY: snap.y,\n color: spectra[spIdx]?.color ?? getSpectrumColor(spIdx),\n });\n onCrosshairMoveRef.current?.(snap.x, snap.y);\n } else {\n setSnapPointState(null);\n onCrosshairMoveRef.current?.(dataX, dataY);\n }\n } else {\n onCrosshairMoveRef.current?.(dataX, dataY);\n }\n },\n [zoomedXScale, zoomedYScale, config.showCrosshair, snapCrosshair, spectra],\n );\n\n const handleMouseLeave = useCallback(() => {\n setCrosshairPos(null);\n setSnapPointState(null);\n }, []);\n\n // Keyboard navigation\n const handleKeyDown = useKeyboardNavigation({\n onZoomIn: zoomIn,\n onZoomOut: zoomOut,\n onReset: resetZoom,\n });\n\n // ARIA description\n const chartDescription = useMemo(\n () => generateChartDescription(spectra.length, labels.xLabel, labels.yLabel),\n [spectra.length, labels.xLabel, labels.yLabel],\n );\n\n const isStacked = config.displayMode === \"stacked\";\n\n // Empty state\n if (spectra.length === 0) {\n return (\n <div\n ref={config.responsive ? resizeRef : undefined}\n style={{\n width: config.responsive ? \"100%\" : width,\n height,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n border: `1px dashed ${colors.gridColor}`,\n borderRadius: 8,\n color: colors.tickColor,\n fontFamily: \"system-ui, sans-serif\",\n fontSize: 14,\n }}\n className={props.className}\n >\n No spectra loaded\n </div>\n );\n }\n\n const toolbarHeight = config.showToolbar ? 37 : 0;\n\n return (\n <div\n ref={config.responsive ? resizeRef : undefined}\n style={{\n width: config.responsive ? \"100%\" : width,\n background: colors.background,\n borderRadius: 4,\n overflow: \"hidden\",\n }}\n className={props.className}\n role=\"img\"\n aria-label={chartDescription}\n tabIndex={0}\n onKeyDown={handleKeyDown}\n >\n {/* Toolbar */}\n {config.showToolbar && (\n <Toolbar\n onZoomIn={zoomIn}\n onZoomOut={zoomOut}\n onReset={resetZoom}\n isZoomed={zoomState.isZoomed}\n theme={theme}\n />\n )}\n\n {/* Legend (top position) */}\n {config.showLegend && config.legendPosition === \"top\" && (\n <Legend\n spectra={spectra}\n theme={theme}\n position=\"top\"\n onToggleVisibility={onToggleVisibility}\n onHighlight={setHighlightedId}\n highlightedId={highlightedId}\n />\n )}\n\n {/* Chart area wrapped in DropZone */}\n <DropZone\n enabled={config.enableDragDrop}\n theme={theme}\n width={width}\n height={height - toolbarHeight}\n onDrop={onFileDrop}\n >\n {isStacked ? (\n /* Stacked mode: each spectrum in its own panel */\n <svg\n width={width}\n height={height - toolbarHeight}\n style={{ position: \"absolute\", top: 0, left: 0 }}\n >\n <g transform={`translate(${margin.left}, ${margin.top})`}>\n <StackedView\n spectra={spectra}\n xScale={zoomedXScale}\n plotWidth={plotWidth}\n plotHeight={plotHeight}\n margin={margin}\n theme={theme}\n showGrid={config.showGrid}\n xLabel={labels.xLabel}\n yLabel={labels.yLabel}\n />\n\n {/* Zoom/pan interaction rect */}\n <rect\n ref={zoomRef}\n x={0}\n y={0}\n width={plotWidth}\n height={plotHeight}\n fill=\"transparent\"\n style={{ cursor: \"grab\" }}\n onMouseMove={handleMouseMove}\n onMouseLeave={handleMouseLeave}\n />\n </g>\n </svg>\n ) : (\n /* Overlay mode: all spectra on single canvas */\n <>\n {/* Canvas layer for spectral data (behind SVG) */}\n <div\n style={{\n position: \"absolute\",\n top: margin.top,\n left: margin.left,\n width: plotWidth,\n height: plotHeight,\n overflow: \"hidden\",\n }}\n >\n <SpectrumCanvas\n ref={canvasRef}\n spectra={spectra}\n xScale={zoomedXScale}\n yScale={zoomedYScale}\n width={plotWidth}\n height={plotHeight}\n highlightedId={highlightedId ?? undefined}\n />\n </div>\n\n {/* SVG overlay for axes, annotations, crosshair */}\n <svg\n width={width}\n height={height - toolbarHeight}\n style={{ position: \"absolute\", top: 0, left: 0 }}\n >\n <g transform={`translate(${margin.left}, ${margin.top})`}>\n {/* Axes and grid */}\n <AxisLayer\n xScale={zoomedXScale}\n yScale={zoomedYScale}\n width={plotWidth}\n height={plotHeight}\n xLabel={labels.xLabel}\n yLabel={labels.yLabel}\n showGrid={config.showGrid}\n colors={colors}\n />\n\n {/* Clip path for plot area content */}\n <defs>\n <clipPath id={clipId}>\n <rect x={0} y={0} width={plotWidth} height={plotHeight} />\n </clipPath>\n </defs>\n\n <g clipPath={`url(#${clipId})`}>\n {/* Region highlights */}\n {regions.length > 0 && (\n <RegionSelector\n regions={regions}\n xScale={zoomedXScale}\n height={plotHeight}\n colors={colors}\n />\n )}\n\n {/* Peak markers */}\n {peaks.length > 0 && (\n <PeakMarkers\n peaks={peaks}\n xScale={zoomedXScale}\n yScale={zoomedYScale}\n colors={colors}\n onPeakClick={onPeakClick}\n />\n )}\n </g>\n\n {/* Annotations */}\n {annotations.length > 0 && (\n <AnnotationLayer\n annotations={annotations}\n xScale={zoomedXScale}\n yScale={zoomedYScale}\n colors={colors}\n />\n )}\n\n {/* Crosshair (rendered above data, pointer-events: none) */}\n {config.showCrosshair && (\n <Crosshair\n position={crosshairPos}\n width={plotWidth}\n height={plotHeight}\n colors={colors}\n snapPoint={snapPointState}\n />\n )}\n\n {/* Pending region highlight */}\n {pendingRegion && (\n <rect\n x={zoomedXScale(pendingRegion.xStart)}\n y={0}\n width={Math.abs(\n zoomedXScale(pendingRegion.xEnd) -\n zoomedXScale(pendingRegion.xStart),\n )}\n height={plotHeight}\n fill={colors.regionFill}\n stroke={colors.regionStroke}\n strokeWidth={1}\n pointerEvents=\"none\"\n />\n )}\n\n {/* Zoom/pan + crosshair interaction rect */}\n <rect\n ref={zoomRef}\n x={0}\n y={0}\n width={plotWidth}\n height={plotHeight}\n fill=\"transparent\"\n style={{ cursor: config.showCrosshair ? \"crosshair\" : \"grab\" }}\n onMouseDown={regionMouseDown}\n onMouseMove={(e) => {\n handleMouseMove(e);\n regionMouseMove(e);\n }}\n onMouseUp={regionMouseUp}\n onMouseLeave={handleMouseLeave}\n />\n </g>\n </svg>\n </>\n )}\n </DropZone>\n\n {/* Legend (bottom position) */}\n {config.showLegend && config.legendPosition === \"bottom\" && (\n <Legend\n spectra={spectra}\n theme={theme}\n position=\"bottom\"\n onToggleVisibility={onToggleVisibility}\n onHighlight={setHighlightedId}\n highlightedId={highlightedId}\n />\n )}\n </div>\n );\n}\n","/**\n * D3 scale factories for spectral axes.\n *\n * Handles reversed x-axis (standard for IR wavenumber display)\n * and automatic domain computation from spectral data.\n */\n\nimport { scaleLinear } from \"d3-scale\";\nimport { extent } from \"d3-array\";\nimport type { Spectrum, Margin } from \"../types\";\n\n/** Padding factor applied to y-axis domain (5% on each side). */\nconst Y_PADDING = 0.05;\n\n/**\n * Compute the x-axis extent across all visible spectra.\n */\nexport function computeXExtent(spectra: Spectrum[]): [number, number] {\n let globalMin = Infinity;\n let globalMax = -Infinity;\n\n for (const s of spectra) {\n if (s.visible === false) continue;\n const [min, max] = extent(s.x as number[]) as [number, number];\n if (min < globalMin) globalMin = min;\n if (max > globalMax) globalMax = max;\n }\n\n if (!isFinite(globalMin)) return [0, 1];\n return [globalMin, globalMax];\n}\n\n/**\n * Compute the y-axis extent across all visible spectra with padding.\n */\nexport function computeYExtent(spectra: Spectrum[]): [number, number] {\n let globalMin = Infinity;\n let globalMax = -Infinity;\n\n for (const s of spectra) {\n if (s.visible === false) continue;\n const [min, max] = extent(s.y as number[]) as [number, number];\n if (min < globalMin) globalMin = min;\n if (max > globalMax) globalMax = max;\n }\n\n if (!isFinite(globalMin)) return [0, 1];\n\n const range = globalMax - globalMin;\n const pad = range * Y_PADDING;\n return [globalMin - pad, globalMax + pad];\n}\n\n/**\n * Create an x-axis scale.\n *\n * When `reverseX` is true, the domain is reversed so higher wavenumbers\n * appear on the left (standard IR convention).\n */\nexport function createXScale(\n domain: [number, number],\n width: number,\n margin: Margin,\n reverseX: boolean,\n) {\n const plotWidth = width - margin.left - margin.right;\n const d = reverseX ? [domain[1], domain[0]] : domain;\n return scaleLinear().domain(d).range([0, plotWidth]);\n}\n\n/**\n * Create a y-axis scale (always low values at bottom, high at top).\n */\nexport function createYScale(\n domain: [number, number],\n height: number,\n margin: Margin,\n) {\n const plotHeight = height - margin.top - margin.bottom;\n return scaleLinear().domain(domain).range([plotHeight, 0]);\n}\n","/**\n * Default color palette for rendering multiple spectra.\n *\n * Colors are chosen for good contrast on both light and dark backgrounds,\n * and are distinguishable for common forms of color blindness.\n */\n\n/** Default spectrum color palette (10 colors). */\nexport const SPECTRUM_COLORS = [\n \"#2563eb\", // blue\n \"#dc2626\", // red\n \"#16a34a\", // green\n \"#9333ea\", // purple\n \"#ea580c\", // orange\n \"#0891b2\", // cyan\n \"#be185d\", // pink\n \"#854d0e\", // brown\n \"#4f46e5\", // indigo\n \"#65a30d\", // lime\n] as const;\n\n/** Theme color definition. */\nexport interface ThemeColors {\n background: string;\n axisColor: string;\n gridColor: string;\n tickColor: string;\n labelColor: string;\n crosshairColor: string;\n regionFill: string;\n regionStroke: string;\n tooltipBg: string;\n tooltipBorder: string;\n tooltipText: string;\n}\n\n/** Light theme colors. */\nexport const LIGHT_THEME: ThemeColors = {\n background: \"#ffffff\",\n axisColor: \"#374151\",\n gridColor: \"#e5e7eb\",\n tickColor: \"#6b7280\",\n labelColor: \"#111827\",\n crosshairColor: \"#9ca3af\",\n regionFill: \"rgba(37, 99, 235, 0.1)\",\n regionStroke: \"rgba(37, 99, 235, 0.4)\",\n tooltipBg: \"#ffffff\",\n tooltipBorder: \"#d1d5db\",\n tooltipText: \"#111827\",\n};\n\n/** Dark theme colors. */\nexport const DARK_THEME: ThemeColors = {\n background: \"#111827\",\n axisColor: \"#d1d5db\",\n gridColor: \"#374151\",\n tickColor: \"#9ca3af\",\n labelColor: \"#f9fafb\",\n crosshairColor: \"#6b7280\",\n regionFill: \"rgba(96, 165, 250, 0.15)\",\n regionStroke: \"rgba(96, 165, 250, 0.5)\",\n tooltipBg: \"#1f2937\",\n tooltipBorder: \"#4b5563\",\n tooltipText: \"#f9fafb\",\n};\n\n/**\n * Get the color for a spectrum at the given index.\n *\n * Cycles through the palette if index exceeds palette length.\n */\nexport function getSpectrumColor(index: number): string {\n return SPECTRUM_COLORS[index % SPECTRUM_COLORS.length];\n}\n\n/**\n * Get theme colors for the given theme name.\n */\nexport function getThemeColors(theme: \"light\" | \"dark\"): ThemeColors {\n return theme === \"dark\" ? DARK_THEME : LIGHT_THEME;\n}\n","/**\n * Hook for zoom and pan behavior backed by d3-zoom.\n *\n * Provides smooth mouse wheel zoom, click-drag pan, and double-click\n * reset. Works with both the SVG overlay and Canvas data layers.\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { zoom, type ZoomBehavior, zoomIdentity, type ZoomTransform } from \"d3-zoom\";\nimport { select } from \"d3-selection\";\nimport \"d3-transition\";\nimport type { ScaleLinear } from \"d3-scale\";\n\nexport interface ZoomPanState {\n /** Current d3 zoom transform. */\n transform: ZoomTransform;\n /** Whether the view is currently zoomed (not at identity). */\n isZoomed: boolean;\n}\n\nexport interface UseZoomPanOptions {\n /** Width of the plot area (excluding margins). */\n plotWidth: number;\n /** Height of the plot area (excluding margins). */\n plotHeight: number;\n /** Base x-scale (unzoomed). */\n xScale: ScaleLinear<number, number>;\n /** Base y-scale (unzoomed). */\n yScale: ScaleLinear<number, number>;\n /** Maximum zoom factor. */\n scaleExtent?: [number, number];\n /** Whether zoom/pan is enabled. */\n enabled?: boolean;\n /** Callback when the view changes. */\n onViewChange?: (xDomain: [number, number], yDomain: [number, number]) => void;\n}\n\nexport interface UseZoomPanReturn {\n /** Ref to attach to the interaction overlay element. */\n zoomRef: React.RefObject<SVGRectElement | null>;\n /** Current zoom/pan state. */\n state: ZoomPanState;\n /** Zoomed (rescaled) x-scale. */\n zoomedXScale: ScaleLinear<number, number>;\n /** Zoomed (rescaled) y-scale. */\n zoomedYScale: ScaleLinear<number, number>;\n /** Reset zoom to initial view. */\n resetZoom: () => void;\n /** Zoom in by a fixed step. */\n zoomIn: () => void;\n /** Zoom out by a fixed step. */\n zoomOut: () => void;\n}\n\n/** Zoom step multiplier for zoomIn/zoomOut. */\nconst ZOOM_STEP = 1.5;\n\nexport function useZoomPan(options: UseZoomPanOptions): UseZoomPanReturn {\n const {\n plotWidth,\n plotHeight,\n xScale,\n yScale,\n scaleExtent = [1, 50],\n enabled = true,\n onViewChange,\n } = options;\n\n const zoomRef = useRef<SVGRectElement | null>(null);\n const zoomBehaviorRef = useRef<ZoomBehavior<SVGRectElement, unknown> | null>(null);\n\n // Store callbacks and config in refs to avoid effect re-runs (REACT-2 fix)\n const onViewChangeRef = useRef(onViewChange);\n onViewChangeRef.current = onViewChange;\n const scaleExtentRef = useRef(scaleExtent);\n scaleExtentRef.current = scaleExtent;\n\n const [transform, setTransform] = useState<ZoomTransform>(zoomIdentity);\n\n // Memoize rescaled axes from the current transform (BUG-4 fix)\n const zoomedXScale = useMemo(\n () => transform.rescaleX(xScale.copy()),\n [transform, xScale],\n );\n const zoomedYScale = useMemo(\n () => transform.rescaleY(yScale.copy()),\n [transform, yScale],\n );\n\n // Set up d3-zoom behavior\n useEffect(() => {\n const element = zoomRef.current;\n if (!element || !enabled) return;\n\n const zoomBehavior = zoom<SVGRectElement, unknown>()\n .scaleExtent(scaleExtentRef.current)\n .extent([\n [0, 0],\n [plotWidth, plotHeight],\n ])\n .translateExtent([\n [-Infinity, -Infinity],\n [Infinity, Infinity],\n ])\n .on(\"zoom\", (event) => {\n const newTransform = event.transform as ZoomTransform;\n setTransform(newTransform);\n\n if (onViewChangeRef.current) {\n const newXScale = newTransform.rescaleX(xScale.copy());\n const newYScale = newTransform.rescaleY(yScale.copy());\n onViewChangeRef.current(\n newXScale.domain() as [number, number],\n newYScale.domain() as [number, number],\n );\n }\n });\n\n zoomBehaviorRef.current = zoomBehavior;\n\n select(element).call(zoomBehavior);\n\n // Double-click to reset\n select(element).on(\"dblclick.zoom\", () => {\n select(element).transition().duration(300).call(zoomBehavior.transform, zoomIdentity);\n });\n\n // Stale ref fix: use captured `element` instead of zoomRef.current\n return () => {\n select(element).on(\".zoom\", null);\n };\n }, [plotWidth, plotHeight, enabled, xScale, yScale]);\n\n const resetZoom = useCallback(() => {\n if (!zoomRef.current || !zoomBehaviorRef.current) return;\n select(zoomRef.current)\n .transition()\n .duration(300)\n .call(zoomBehaviorRef.current.transform, zoomIdentity);\n }, []);\n\n const zoomIn = useCallback(() => {\n if (!zoomRef.current || !zoomBehaviorRef.current) return;\n select(zoomRef.current)\n .transition()\n .duration(200)\n .call(zoomBehaviorRef.current.scaleBy, ZOOM_STEP);\n }, []);\n\n const zoomOut = useCallback(() => {\n if (!zoomRef.current || !zoomBehaviorRef.current) return;\n select(zoomRef.current)\n .transition()\n .duration(200)\n .call(zoomBehaviorRef.current.scaleBy, 1 / ZOOM_STEP);\n }, []);\n\n return {\n zoomRef,\n state: {\n transform,\n isZoomed: transform.k !== 1 || transform.x !== 0 || transform.y !== 0,\n },\n zoomedXScale,\n zoomedYScale,\n resetZoom,\n zoomIn,\n zoomOut,\n };\n}\n","/**\n * Canvas rendering layer for spectral data.\n *\n * Uses HTML5 Canvas 2D for high-performance rendering of spectral lines.\n * Redraws on zoom/pan transform changes and spectrum data changes.\n */\n\nimport { forwardRef, useEffect, useImperativeHandle, useRef } from \"react\";\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { Spectrum } from \"../../types\";\nimport { drawAllSpectra } from \"../../utils/rendering\";\n\nexport interface SpectrumCanvasProps {\n /** Spectra to render. */\n spectra: Spectrum[];\n /** X-axis scale (already zoomed). */\n xScale: ScaleLinear<number, number>;\n /** Y-axis scale (already zoomed). */\n yScale: ScaleLinear<number, number>;\n /** Canvas width in pixels. */\n width: number;\n /** Canvas height in pixels. */\n height: number;\n /** ID of the currently highlighted spectrum. */\n highlightedId?: string;\n}\n\nexport const SpectrumCanvas = forwardRef<HTMLCanvasElement, SpectrumCanvasProps>(\n function SpectrumCanvas(\n { spectra, xScale, yScale, width, height, highlightedId },\n ref,\n ) {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const dprRef = useRef(1);\n\n // Expose the internal canvas ref to parent via forwarded ref\n useImperativeHandle(ref, () => canvasRef.current!, []);\n\n // Set up canvas DPR only when dimensions change (avoids flicker on zoom)\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const dpr = window.devicePixelRatio || 1;\n dprRef.current = dpr;\n canvas.width = width * dpr;\n canvas.height = height * dpr;\n }, [width, height]);\n\n // Redraw spectra when data or scales change\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return;\n\n const dpr = dprRef.current;\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\n drawAllSpectra(ctx, spectra, xScale, yScale, width, height, highlightedId);\n }, [spectra, xScale, yScale, width, height, highlightedId]);\n\n return (\n <canvas\n ref={canvasRef}\n style={{\n width,\n height,\n position: \"absolute\",\n top: 0,\n left: 0,\n pointerEvents: \"none\",\n }}\n />\n );\n },\n);\n","/**\n * Largest-Triangle-Three-Buckets (LTTB) downsampling algorithm.\n *\n * LTTB produces visually superior downsampled representations compared to\n * simple min-max binning. It works by dividing data into buckets and\n * selecting the point in each bucket that forms the largest triangle with\n * the selected points in adjacent buckets.\n *\n * Reference: Sveinn Steinarsson, \"Downsampling Time Series for Visual\n * Representation\" (2013).\n *\n * @module lttb\n */\n\n/** A downsampled point with its original index. */\nexport interface LTTBPoint {\n /** Pixel x coordinate. */\n px: number;\n /** Pixel y coordinate. */\n py: number;\n /** Original index in the source arrays. */\n index: number;\n}\n\n/**\n * Downsample data using the LTTB algorithm.\n *\n * @param x - Source x-values\n * @param y - Source y-values\n * @param startIdx - Start index (inclusive) in the source arrays\n * @param endIdx - End index (exclusive) in the source arrays\n * @param xScale - Function mapping data x to pixel x\n * @param yScale - Function mapping data y to pixel y\n * @param targetCount - Desired number of output points\n * @returns Array of downsampled points\n */\nexport function lttbDownsample(\n x: Float64Array | number[],\n y: Float64Array | number[],\n startIdx: number,\n endIdx: number,\n xScale: (v: number) => number,\n yScale: (v: number) => number,\n targetCount: number,\n): LTTBPoint[] {\n const n = endIdx - startIdx;\n\n // If fewer points than target, return all\n if (n <= targetCount) {\n const result: LTTBPoint[] = [];\n for (let i = startIdx; i < endIdx; i++) {\n result.push({\n px: xScale(x[i] as number),\n py: yScale(y[i] as number),\n index: i,\n });\n }\n return result;\n }\n\n // Always include first and last points\n const result: LTTBPoint[] = [];\n result.push({\n px: xScale(x[startIdx] as number),\n py: yScale(y[startIdx] as number),\n index: startIdx,\n });\n\n // Number of buckets (excluding first and last points)\n const bucketCount = targetCount - 2;\n const bucketSize = (n - 2) / bucketCount;\n\n let prevSelectedIdx = startIdx;\n\n for (let bucket = 0; bucket < bucketCount; bucket++) {\n // Current bucket range\n const bucketStart = startIdx + 1 + Math.floor(bucket * bucketSize);\n const bucketEnd = startIdx + 1 + Math.min(\n Math.floor((bucket + 1) * bucketSize),\n n - 2,\n );\n\n // Next bucket average (used as the third vertex of the triangle)\n const nextBucketStart = bucketEnd;\n const nextBucketEnd = startIdx + 1 + Math.min(\n Math.floor((bucket + 2) * bucketSize),\n n - 2,\n );\n // For the last bucket, use the last data point as the average\n let avgX: number;\n let avgY: number;\n\n if (bucket === bucketCount - 1) {\n avgX = xScale(x[endIdx - 1] as number);\n avgY = yScale(y[endIdx - 1] as number);\n } else {\n avgX = 0;\n avgY = 0;\n const avgCount = nextBucketEnd - nextBucketStart;\n for (let i = nextBucketStart; i < nextBucketEnd; i++) {\n avgX += xScale(x[i] as number);\n avgY += yScale(y[i] as number);\n }\n if (avgCount > 0) {\n avgX /= avgCount;\n avgY /= avgCount;\n }\n }\n\n // Previous selected point (first vertex)\n const prevPx = xScale(x[prevSelectedIdx] as number);\n const prevPy = yScale(y[prevSelectedIdx] as number);\n\n // Find point in current bucket with maximum triangle area\n let maxArea = -1;\n let bestIdx = bucketStart;\n\n for (let i = bucketStart; i < bucketEnd; i++) {\n const px = xScale(x[i] as number);\n const py = yScale(y[i] as number);\n\n // Triangle area (using cross product formula, no /2 needed for comparison)\n const area = Math.abs(\n (prevPx - avgX) * (py - prevPy) -\n (prevPx - px) * (avgY - prevPy),\n );\n\n if (area > maxArea) {\n maxArea = area;\n bestIdx = i;\n }\n }\n\n result.push({\n px: xScale(x[bestIdx] as number),\n py: yScale(y[bestIdx] as number),\n index: bestIdx,\n });\n prevSelectedIdx = bestIdx;\n }\n\n // Add last point\n result.push({\n px: xScale(x[endIdx - 1] as number),\n py: yScale(y[endIdx - 1] as number),\n index: endIdx - 1,\n });\n\n return result;\n}\n","/**\n * Canvas 2D rendering utilities for drawing spectral lines.\n *\n * Canvas is used for the data-heavy spectral line rendering (10K+ points)\n * while SVG handles axes, annotations, and interactive overlays.\n */\n\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { Spectrum, LineStyle } from \"../types\";\nimport { getSpectrumColor } from \"./colors\";\nimport { lttbDownsample } from \"./lttb\";\n\n/** Default line width for spectrum rendering. */\nconst LINE_WIDTH = 1.5;\n\n/** Canvas dash patterns for line styles. */\nconst CANVAS_DASH_PATTERNS: Record<LineStyle, number[]> = {\n solid: [],\n dashed: [8, 4],\n dotted: [2, 2],\n \"dash-dot\": [8, 4, 2, 4],\n};\n\n/**\n * Threshold: if visible points exceed this count, apply LTTB downsampling.\n * Target output is plotWidth * 2 points — enough visual fidelity for\n * sub-pixel accuracy while dramatically reducing draw calls.\n */\nconst DECIMATION_THRESHOLD = 2000;\n\n/**\n * Clear the canvas and set up for drawing.\n */\nexport function clearCanvas(\n ctx: CanvasRenderingContext2D,\n width: number,\n height: number,\n): void {\n ctx.clearRect(0, 0, width, height);\n}\n\n/**\n * Draw a single spectrum as a line path on the canvas.\n *\n * Uses beginPath/lineTo for maximum performance with large point counts.\n * Applies min-max decimation when point count exceeds DECIMATION_THRESHOLD.\n */\nexport function drawSpectrum(\n ctx: CanvasRenderingContext2D,\n spectrum: Spectrum,\n index: number,\n xScale: ScaleLinear<number, number>,\n yScale: ScaleLinear<number, number>,\n plotWidth: number,\n options?: {\n highlighted?: boolean;\n opacity?: number;\n },\n): void {\n const { highlighted = false, opacity = 1.0 } = options ?? {};\n const n = Math.min(spectrum.x.length, spectrum.y.length);\n if (n < 2) return;\n\n const color = spectrum.color ?? getSpectrumColor(index);\n const baseWidth = spectrum.lineWidth ?? LINE_WIDTH;\n const lineWidth = highlighted ? baseWidth + 1 : baseWidth;\n const dashPattern = CANVAS_DASH_PATTERNS[spectrum.lineStyle ?? \"solid\"] ?? [];\n\n // Get visible x-domain for culling\n const [xMin, xMax] = xScale.domain() as [number, number];\n const domainMin = Math.min(xMin, xMax);\n const domainMax = Math.max(xMin, xMax);\n\n // Find range of visible indices (with 1-point margin for continuity)\n let startIdx = 0;\n let endIdx = n;\n for (let i = 0; i < n; i++) {\n if ((spectrum.x[i] as number) >= domainMin || (i < n - 1 && (spectrum.x[i + 1] as number) >= domainMin)) {\n startIdx = Math.max(0, i - 1);\n break;\n }\n }\n for (let i = n - 1; i >= 0; i--) {\n if ((spectrum.x[i] as number) <= domainMax || (i > 0 && (spectrum.x[i - 1] as number) <= domainMax)) {\n endIdx = Math.min(n, i + 2);\n break;\n }\n }\n\n const visibleCount = endIdx - startIdx;\n\n ctx.save();\n ctx.beginPath();\n ctx.strokeStyle = color;\n ctx.lineWidth = lineWidth;\n ctx.globalAlpha = opacity;\n ctx.lineJoin = \"round\";\n ctx.setLineDash(dashPattern);\n\n if (visibleCount > DECIMATION_THRESHOLD) {\n // LTTB downsampled path: visually optimal point selection\n const targetPoints = Math.max(Math.ceil(plotWidth * 2), 200);\n const points = lttbDownsample(spectrum.x, spectrum.y, startIdx, endIdx, xScale, yScale, targetPoints);\n if (points.length > 0) {\n ctx.moveTo(points[0].px, points[0].py);\n for (let i = 1; i < points.length; i++) {\n ctx.lineTo(points[i].px, points[i].py);\n }\n }\n } else {\n // Direct path: draw all visible points\n let started = false;\n for (let i = startIdx; i < endIdx; i++) {\n const px = xScale(spectrum.x[i] as number);\n const py = yScale(spectrum.y[i] as number);\n if (!started) {\n ctx.moveTo(px, py);\n started = true;\n } else {\n ctx.lineTo(px, py);\n }\n }\n }\n\n ctx.stroke();\n ctx.restore();\n}\n\n/**\n * Draw all visible spectra onto the canvas.\n */\nexport function drawAllSpectra(\n ctx: CanvasRenderingContext2D,\n spectra: Spectrum[],\n xScale: ScaleLinear<number, number>,\n yScale: ScaleLinear<number, number>,\n width: number,\n height: number,\n highlightedId?: string,\n): void {\n clearCanvas(ctx, width, height);\n\n spectra.forEach((spectrum, index) => {\n if (spectrum.visible === false) return;\n\n drawSpectrum(ctx, spectrum, index, xScale, yScale, width, {\n highlighted: spectrum.id === highlightedId,\n opacity: highlightedId && spectrum.id !== highlightedId ? 0.3 : 1.0,\n });\n });\n}\n","/**\n * SVG axis layer rendering X and Y axes with labels and grid lines.\n *\n * Built with plain SVG for minimal bundle size. Handles reversed x-axis\n * (standard for IR wavenumber display).\n */\n\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { ThemeColors } from \"../../utils/colors\";\n\nexport interface AxisLayerProps {\n /** X-axis scale (already zoomed). */\n xScale: ScaleLinear<number, number>;\n /** Y-axis scale (already zoomed). */\n yScale: ScaleLinear<number, number>;\n /** Plot area width (excluding margins). */\n width: number;\n /** Plot area height (excluding margins). */\n height: number;\n /** X-axis label. */\n xLabel?: string;\n /** Y-axis label. */\n yLabel?: string;\n /** Show grid lines. */\n showGrid?: boolean;\n /** Theme colors. */\n colors: ThemeColors;\n}\n\n/** Number of tick marks to show on each axis. */\nconst TICK_COUNT = 8;\n\n/**\n * Generate evenly-spaced tick values for a linear scale.\n */\nfunction generateTicks(scale: ScaleLinear<number, number>, count: number): number[] {\n const [d0, d1] = scale.domain() as [number, number];\n const min = Math.min(d0, d1);\n const max = Math.max(d0, d1);\n const step = (max - min) / (count - 1);\n return Array.from({ length: count }, (_, i) => min + i * step);\n}\n\n/**\n * Format a tick value for display.\n */\nfunction formatTick(value: number): string {\n if (Math.abs(value) >= 1000) return Math.round(value).toString();\n if (Math.abs(value) >= 1) return value.toFixed(1);\n if (Math.abs(value) >= 0.01) return value.toFixed(3);\n return value.toExponential(1);\n}\n\nexport function AxisLayer({\n xScale,\n yScale,\n width,\n height,\n xLabel,\n yLabel,\n showGrid = true,\n colors,\n}: AxisLayerProps) {\n const xTicks = generateTicks(xScale, TICK_COUNT);\n const yTicks = generateTicks(yScale, TICK_COUNT - 2);\n\n return (\n <g>\n {/* Grid lines */}\n {showGrid && (\n <g>\n {xTicks.map((tick) => (\n <line\n key={`xgrid-${tick}`}\n x1={xScale(tick)}\n x2={xScale(tick)}\n y1={0}\n y2={height}\n stroke={colors.gridColor}\n strokeWidth={0.5}\n />\n ))}\n {yTicks.map((tick) => (\n <line\n key={`ygrid-${tick}`}\n x1={0}\n x2={width}\n y1={yScale(tick)}\n y2={yScale(tick)}\n stroke={colors.gridColor}\n strokeWidth={0.5}\n />\n ))}\n </g>\n )}\n\n {/* X-axis */}\n <g transform={`translate(0, ${height})`}>\n <line x1={0} x2={width} y1={0} y2={0} stroke={colors.axisColor} />\n {xTicks.map((tick) => (\n <g key={`xtick-${tick}`} transform={`translate(${xScale(tick)}, 0)`}>\n <line y1={0} y2={6} stroke={colors.axisColor} />\n <text\n y={20}\n textAnchor=\"middle\"\n fill={colors.tickColor}\n fontSize={11}\n fontFamily=\"system-ui, sans-serif\"\n >\n {formatTick(tick)}\n </text>\n </g>\n ))}\n {xLabel && (\n <text\n x={width / 2}\n y={42}\n textAnchor=\"middle\"\n fill={colors.labelColor}\n fontSize={13}\n fontFamily=\"system-ui, sans-serif\"\n >\n {xLabel}\n </text>\n )}\n </g>\n\n {/* Y-axis */}\n <g>\n <line x1={0} x2={0} y1={0} y2={height} stroke={colors.axisColor} />\n {yTicks.map((tick) => (\n <g key={`ytick-${tick}`} transform={`translate(0, ${yScale(tick)})`}>\n <line x1={-6} x2={0} stroke={colors.axisColor} />\n <text\n x={-10}\n textAnchor=\"end\"\n dominantBaseline=\"middle\"\n fill={colors.tickColor}\n fontSize={11}\n fontFamily=\"system-ui, sans-serif\"\n >\n {formatTick(tick)}\n </text>\n </g>\n ))}\n {yLabel && (\n <text\n transform={`translate(-50, ${height / 2}) rotate(-90)`}\n textAnchor=\"middle\"\n fill={colors.labelColor}\n fontSize={13}\n fontFamily=\"system-ui, sans-serif\"\n >\n {yLabel}\n </text>\n )}\n </g>\n </g>\n );\n}\n","/**\n * Peak annotation markers rendered as SVG overlays.\n *\n * Displays small triangles at peak positions with wavenumber labels.\n * Supports click interaction for peak selection.\n */\n\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { Peak } from \"../../types\";\nimport type { ThemeColors } from \"../../utils/colors\";\n\nexport interface PeakMarkersProps {\n /** Peaks to display. */\n peaks: Peak[];\n /** X-axis scale (zoomed). */\n xScale: ScaleLinear<number, number>;\n /** Y-axis scale (zoomed). */\n yScale: ScaleLinear<number, number>;\n /** Theme colors. */\n colors: ThemeColors;\n /** Callback when a peak is clicked. */\n onPeakClick?: (peak: Peak) => void;\n}\n\n/** Size of the peak marker triangle. */\nconst MARKER_SIZE = 5;\n\n/** Vertical offset for the label above the marker. */\nconst LABEL_OFFSET = 14;\n\nexport function PeakMarkers({\n peaks,\n xScale,\n yScale,\n colors,\n onPeakClick,\n}: PeakMarkersProps) {\n // Get visible domain to cull off-screen peaks\n const [xMin, xMax] = xScale.domain() as [number, number];\n const domainMin = Math.min(xMin, xMax);\n const domainMax = Math.max(xMin, xMax);\n\n const visiblePeaks = peaks.filter(\n (p) => p.x >= domainMin && p.x <= domainMax,\n );\n\n return (\n <g className=\"spectraview-peaks\">\n {visiblePeaks.map((peak, i) => {\n const px = xScale(peak.x);\n const py = yScale(peak.y);\n\n return (\n <g\n key={`peak-${peak.x}-${i}`}\n transform={`translate(${px}, ${py})`}\n style={{ cursor: onPeakClick ? \"pointer\" : \"default\" }}\n onClick={() => onPeakClick?.(peak)}\n >\n {/* Triangle marker pointing down */}\n <polygon\n points={`0,${-MARKER_SIZE} ${-MARKER_SIZE},${-MARKER_SIZE * 2.5} ${MARKER_SIZE},${-MARKER_SIZE * 2.5}`}\n fill={colors.labelColor}\n opacity={0.8}\n />\n {/* Wavenumber label */}\n {peak.label && (\n <text\n y={-MARKER_SIZE * 2.5 - LABEL_OFFSET}\n textAnchor=\"middle\"\n fill={colors.labelColor}\n fontSize={10}\n fontFamily=\"system-ui, sans-serif\"\n fontWeight={500}\n >\n {peak.label}\n </text>\n )}\n </g>\n );\n })}\n </g>\n );\n}\n","/**\n * Region selection overlay for click-drag x-axis region selection.\n *\n * Renders highlighted rectangular regions on the spectrum and\n * handles mouse interaction for creating new regions.\n */\n\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { Region } from \"../../types\";\nimport type { ThemeColors } from \"../../utils/colors\";\n\nexport interface RegionSelectorProps {\n /** Existing regions to display. */\n regions: Region[];\n /** X-axis scale (zoomed). */\n xScale: ScaleLinear<number, number>;\n /** Plot area height. */\n height: number;\n /** Theme colors. */\n colors: ThemeColors;\n}\n\nexport function RegionSelector({\n regions,\n xScale,\n height,\n colors,\n}: RegionSelectorProps) {\n return (\n <g className=\"spectraview-regions\">\n {regions.map((region, i) => {\n const x1 = xScale(region.xStart);\n const x2 = xScale(region.xEnd);\n const left = Math.min(x1, x2);\n const w = Math.abs(x2 - x1);\n\n return (\n <g key={`region-${i}`}>\n <rect\n x={left}\n y={0}\n width={w}\n height={height}\n fill={region.color ?? colors.regionFill}\n stroke={colors.regionStroke}\n strokeWidth={1}\n />\n {region.label && (\n <text\n x={left + w / 2}\n y={12}\n textAnchor=\"middle\"\n fill={colors.labelColor}\n fontSize={10}\n fontFamily=\"system-ui, sans-serif\"\n >\n {region.label}\n </text>\n )}\n </g>\n );\n })}\n </g>\n );\n}\n","/**\n * Crosshair overlay showing current cursor position with coordinate readout.\n *\n * Pure rendering component — mouse tracking is handled by the parent\n * SpectraView component on the shared zoom/interaction rect.\n */\n\nimport type { ThemeColors } from \"../../utils/colors\";\n\n/** Position data for the crosshair. */\nexport interface CrosshairPosition {\n /** Pixel x coordinate within the plot area. */\n px: number;\n /** Pixel y coordinate within the plot area. */\n py: number;\n /** Data-space x value. */\n dataX: number;\n /** Data-space y value. */\n dataY: number;\n}\n\n/** Snap point data for rendering a snap dot on the nearest spectrum. */\nexport interface SnapPoint {\n /** Pixel x of the snapped data point. */\n px: number;\n /** Pixel y of the snapped data point. */\n py: number;\n /** Data-space x value. */\n dataX: number;\n /** Data-space y value. */\n dataY: number;\n /** Color of the spectrum (for dot fill). */\n color?: string;\n}\n\nexport interface CrosshairProps {\n /** Current crosshair position, or null when not hovering. */\n position: CrosshairPosition | null;\n /** Plot area width. */\n width: number;\n /** Plot area height. */\n height: number;\n /** Theme colors. */\n colors: ThemeColors;\n /** Optional snap point on nearest spectrum. */\n snapPoint?: SnapPoint | null;\n}\n\nexport function Crosshair({\n position,\n width,\n height,\n colors,\n snapPoint,\n}: CrosshairProps) {\n if (!position) return null;\n\n return (\n <g className=\"spectraview-crosshair\" pointerEvents=\"none\">\n {/* Vertical line */}\n <line\n x1={position.px}\n x2={position.px}\n y1={0}\n y2={height}\n stroke={colors.crosshairColor}\n strokeWidth={1}\n strokeDasharray=\"4 4\"\n />\n {/* Horizontal line */}\n <line\n x1={0}\n x2={width}\n y1={position.py}\n y2={position.py}\n stroke={colors.crosshairColor}\n strokeWidth={1}\n strokeDasharray=\"4 4\"\n />\n {/* Snap dot on nearest spectrum */}\n {snapPoint && (\n <circle\n cx={snapPoint.px}\n cy={snapPoint.py}\n r={4}\n fill={snapPoint.color ?? colors.crosshairColor}\n stroke={colors.background}\n strokeWidth={1.5}\n />\n )}\n\n {/* Coordinate readout (shows snapped values when available) */}\n <g\n transform={`translate(${Math.min(position.px + 10, width - 100)}, ${Math.max(position.py - 10, 20)})`}\n >\n <rect\n x={0}\n y={-14}\n width={90}\n height={18}\n rx={3}\n fill={colors.tooltipBg}\n stroke={colors.tooltipBorder}\n strokeWidth={0.5}\n opacity={0.9}\n />\n <text\n x={5}\n y={0}\n fill={colors.tooltipText}\n fontSize={10}\n fontFamily=\"monospace\"\n >\n {formatValue(snapPoint?.dataX ?? position.dataX)},{\" \"}\n {formatValue(snapPoint?.dataY ?? position.dataY)}\n </text>\n </g>\n </g>\n );\n}\n\nfunction formatValue(v: number): string {\n if (Math.abs(v) >= 100) return Math.round(v).toString();\n if (Math.abs(v) >= 1) return v.toFixed(1);\n return v.toFixed(4);\n}\n","/**\n * AnnotationLayer — Renders text annotations at specified data positions.\n *\n * Annotations are positioned in data-space and transformed to pixel-space\n * via the provided scales. Supports optional anchor lines from the\n * annotation text to the data point.\n */\n\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { Annotation } from \"../../types\";\nimport type { ThemeColors } from \"../../utils/colors\";\n\nexport interface AnnotationLayerProps {\n /** Annotations to render. */\n annotations: Annotation[];\n /** X scale (zoomed). */\n xScale: ScaleLinear<number, number>;\n /** Y scale (zoomed). */\n yScale: ScaleLinear<number, number>;\n /** Theme colors. */\n colors: ThemeColors;\n}\n\nexport function AnnotationLayer({\n annotations,\n xScale,\n yScale,\n colors,\n}: AnnotationLayerProps) {\n if (annotations.length === 0) return null;\n\n return (\n <g className=\"spectraview-annotations\" pointerEvents=\"none\">\n {annotations.map((ann) => {\n const px = xScale(ann.x);\n const py = yScale(ann.y);\n const [dx, dy] = ann.offset ?? [0, -20];\n const textX = px + dx;\n const textY = py + dy;\n const fontSize = ann.fontSize ?? 11;\n const color = ann.color ?? colors.tickColor;\n const showLine = ann.showAnchorLine !== false;\n\n return (\n <g key={ann.id}>\n {/* Anchor line */}\n {showLine && (\n <line\n x1={px}\n y1={py}\n x2={textX}\n y2={textY}\n stroke={color}\n strokeWidth={0.75}\n strokeDasharray=\"3 2\"\n opacity={0.6}\n />\n )}\n {/* Anchor dot */}\n <circle cx={px} cy={py} r={2.5} fill={color} opacity={0.8} />\n {/* Text background for readability */}\n <text\n x={textX}\n y={textY}\n fill={colors.background}\n fontSize={fontSize}\n fontFamily=\"system-ui, sans-serif\"\n textAnchor=\"middle\"\n dominantBaseline=\"auto\"\n stroke={colors.background}\n strokeWidth={3}\n strokeLinejoin=\"round\"\n >\n {ann.text}\n </text>\n {/* Annotation text */}\n <text\n x={textX}\n y={textY}\n fill={color}\n fontSize={fontSize}\n fontFamily=\"system-ui, sans-serif\"\n textAnchor=\"middle\"\n dominantBaseline=\"auto\"\n >\n {ann.text}\n </text>\n </g>\n );\n })}\n </g>\n );\n}\n","/**\n * Binary search utilities for snapping crosshair to nearest spectrum data.\n *\n * Given a cursor x-position, finds the closest data point on visible spectra\n * using binary search for O(log n) performance.\n */\n\nimport type { Spectrum } from \"../types\";\n\n/** Result of a snap-to-spectrum search. */\nexport interface SnapResult {\n /** Spectrum ID of the closest match. */\n spectrumId: string;\n /** Index within the spectrum's data arrays. */\n index: number;\n /** Data-space x value of the snapped point. */\n x: number;\n /** Data-space y value of the snapped point. */\n y: number;\n /** Pixel distance from cursor to the snapped point. */\n distance: number;\n}\n\n/**\n * Binary search for the index of the closest x-value in a sorted array.\n *\n * Works with both ascending and descending arrays.\n * Returns the index of the element closest to `target`.\n */\nexport function binarySearchClosest(\n arr: Float64Array | number[],\n target: number,\n length: number,\n): number {\n if (length === 0) return -1;\n if (length === 1) return 0;\n\n // Determine sort direction\n const ascending = (arr[length - 1] as number) >= (arr[0] as number);\n\n let lo = 0;\n let hi = length - 1;\n\n while (lo < hi - 1) {\n const mid = (lo + hi) >>> 1;\n const midVal = arr[mid] as number;\n\n if (ascending) {\n if (midVal <= target) lo = mid;\n else hi = mid;\n } else {\n if (midVal >= target) lo = mid;\n else hi = mid;\n }\n }\n\n // Compare lo and hi to find closest\n const dLo = Math.abs((arr[lo] as number) - target);\n const dHi = Math.abs((arr[hi] as number) - target);\n return dLo <= dHi ? lo : hi;\n}\n\n/**\n * Find the nearest data point across all visible spectra to a given\n * data-space x position. Uses pixel-space distance for ranking.\n */\nexport function snapToNearestSpectrum(\n spectra: Spectrum[],\n dataX: number,\n cursorPy: number,\n xScale: (v: number) => number,\n yScale: (v: number) => number,\n): SnapResult | null {\n let best: SnapResult | null = null;\n\n for (const spectrum of spectra) {\n if (spectrum.visible === false) continue;\n\n const n = Math.min(spectrum.x.length, spectrum.y.length);\n if (n < 2) continue;\n\n const idx = binarySearchClosest(spectrum.x, dataX, n);\n if (idx < 0) continue;\n\n const sx = spectrum.x[idx] as number;\n const sy = spectrum.y[idx] as number;\n\n // Compute pixel distance (primarily y-axis since x is at cursor)\n const pxDist = Math.abs(xScale(sx) - xScale(dataX));\n const pyDist = Math.abs(yScale(sy) - cursorPy);\n const distance = Math.sqrt(pxDist * pxDist + pyDist * pyDist);\n\n if (!best || distance < best.distance) {\n best = {\n spectrumId: spectrum.id,\n index: idx,\n x: sx,\n y: sy,\n distance,\n };\n }\n }\n\n return best;\n}\n","/**\n * Toolbar component with zoom controls and action buttons.\n *\n * Provides zoom in, zoom out, reset, and export controls\n * for the SpectraView component.\n */\n\nimport { memo } from \"react\";\nimport type { Theme } from \"../../types\";\n\nexport interface ToolbarProps {\n /** Zoom in handler. */\n onZoomIn: () => void;\n /** Zoom out handler. */\n onZoomOut: () => void;\n /** Reset zoom handler. */\n onReset: () => void;\n /** Whether the view is currently zoomed. */\n isZoomed: boolean;\n /** Theme. */\n theme: Theme;\n}\n\n/** Inline styles to avoid CSS dependency for the toolbar. */\nconst buttonStyle = (theme: Theme): React.CSSProperties => ({\n display: \"inline-flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: 28,\n height: 28,\n border: `1px solid ${theme === \"dark\" ? \"#4b5563\" : \"#d1d5db\"}`,\n borderRadius: 4,\n background: theme === \"dark\" ? \"#1f2937\" : \"#ffffff\",\n color: theme === \"dark\" ? \"#d1d5db\" : \"#374151\",\n fontSize: 14,\n cursor: \"pointer\",\n padding: 0,\n lineHeight: 1,\n});\n\nconst toolbarStyle = (theme: Theme): React.CSSProperties => ({\n display: \"flex\",\n gap: 4,\n padding: \"4px 0\",\n borderBottom: `1px solid ${theme === \"dark\" ? \"#374151\" : \"#e5e7eb\"}`,\n});\n\nexport const Toolbar = memo(function Toolbar({\n onZoomIn,\n onZoomOut,\n onReset,\n isZoomed,\n theme,\n}: ToolbarProps) {\n return (\n <div style={toolbarStyle(theme)} className=\"spectraview-toolbar\">\n <button\n type=\"button\"\n style={buttonStyle(theme)}\n onClick={onZoomIn}\n title=\"Zoom in\"\n aria-label=\"Zoom in\"\n >\n +\n </button>\n <button\n type=\"button\"\n style={buttonStyle(theme)}\n onClick={onZoomOut}\n title=\"Zoom out\"\n aria-label=\"Zoom out\"\n >\n −\n </button>\n <button\n type=\"button\"\n style={{\n ...buttonStyle(theme),\n opacity: isZoomed ? 1 : 0.4,\n }}\n onClick={onReset}\n disabled={!isZoomed}\n title=\"Reset zoom\"\n aria-label=\"Reset zoom\"\n >\n ↺\n </button>\n </div>\n );\n});\n","/**\n * Legend component showing spectrum names, color swatches, and visibility toggles.\n *\n * Supports hover highlighting and click-to-toggle visibility.\n */\n\nimport { memo } from \"react\";\nimport type { Spectrum, Theme } from \"../../types\";\nimport { getSpectrumColor } from \"../../utils/colors\";\n\n/** Position for the legend relative to the chart. */\nexport type LegendPosition = \"top\" | \"bottom\" | \"left\" | \"right\";\n\nexport interface LegendProps {\n /** Spectra to list in the legend. */\n spectra: Spectrum[];\n /** Theme for styling. */\n theme: Theme;\n /** Legend position. */\n position: LegendPosition;\n /** Callback when a spectrum's visibility is toggled. */\n onToggleVisibility?: (id: string) => void;\n /** Callback when hovering a spectrum in the legend. */\n onHighlight?: (id: string | null) => void;\n /** Currently highlighted spectrum ID. */\n highlightedId?: string | null;\n}\n\nconst containerStyle = (\n theme: Theme,\n position: LegendPosition,\n): React.CSSProperties => ({\n display: \"flex\",\n flexDirection: position === \"left\" || position === \"right\" ? \"column\" : \"row\",\n flexWrap: \"wrap\",\n gap: 6,\n padding: \"4px 8px\",\n fontSize: 12,\n fontFamily: \"system-ui, sans-serif\",\n borderTop:\n position === \"bottom\"\n ? `1px solid ${theme === \"dark\" ? \"#374151\" : \"#e5e7eb\"}`\n : undefined,\n borderBottom:\n position === \"top\"\n ? `1px solid ${theme === \"dark\" ? \"#374151\" : \"#e5e7eb\"}`\n : undefined,\n borderLeft:\n position === \"right\"\n ? `1px solid ${theme === \"dark\" ? \"#374151\" : \"#e5e7eb\"}`\n : undefined,\n borderRight:\n position === \"left\"\n ? `1px solid ${theme === \"dark\" ? \"#374151\" : \"#e5e7eb\"}`\n : undefined,\n});\n\nconst itemStyle = (\n theme: Theme,\n isHidden: boolean,\n isHighlighted: boolean,\n): React.CSSProperties => ({\n display: \"inline-flex\",\n alignItems: \"center\",\n gap: 4,\n cursor: \"pointer\",\n opacity: isHidden ? 0.4 : 1,\n fontWeight: isHighlighted ? 600 : 400,\n color: theme === \"dark\" ? \"#e5e7eb\" : \"#374151\",\n userSelect: \"none\",\n padding: \"2px 4px\",\n borderRadius: 3,\n background: isHighlighted\n ? theme === \"dark\"\n ? \"rgba(255,255,255,0.08)\"\n : \"rgba(0,0,0,0.04)\"\n : \"transparent\",\n transition: \"background 0.15s, opacity 0.15s\",\n});\n\nconst swatchStyle = (\n color: string,\n isHidden: boolean,\n): React.CSSProperties => ({\n width: 12,\n height: 3,\n borderRadius: 1,\n background: color,\n opacity: isHidden ? 0.4 : 1,\n flexShrink: 0,\n});\n\nexport const Legend = memo(function Legend({\n spectra,\n theme,\n position,\n onToggleVisibility,\n onHighlight,\n highlightedId,\n}: LegendProps) {\n if (spectra.length <= 1) return null;\n\n return (\n <div\n className=\"spectraview-legend\"\n style={containerStyle(theme, position)}\n role=\"list\"\n aria-label=\"Spectrum legend\"\n >\n {spectra.map((s, i) => {\n const color = s.color ?? getSpectrumColor(i);\n const isHidden = s.visible === false;\n const isHighlighted = highlightedId === s.id;\n\n return (\n <div\n key={s.id}\n role=\"listitem\"\n style={itemStyle(theme, isHidden, isHighlighted)}\n onClick={() => onToggleVisibility?.(s.id)}\n onMouseEnter={() => onHighlight?.(s.id)}\n onMouseLeave={() => onHighlight?.(null)}\n title={isHidden ? `Show ${s.label}` : `Hide ${s.label}`}\n >\n <span style={swatchStyle(color, isHidden)} />\n <span\n style={{\n textDecoration: isHidden ? \"line-through\" : \"none\",\n maxWidth: 120,\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n whiteSpace: \"nowrap\",\n }}\n >\n {s.label}\n </span>\n </div>\n );\n })}\n </div>\n );\n});\n","/**\n * DropZone component for drag-and-drop file loading.\n *\n * Wraps children with drag event handling and shows a visual\n * overlay when files are dragged over.\n */\n\nimport { useCallback, useState, type ReactNode } from \"react\";\nimport type { Theme } from \"../../types\";\n\nexport interface DropZoneProps {\n /** Whether drag-drop is enabled. */\n enabled: boolean;\n /** Theme for styling the overlay. */\n theme: Theme;\n /** Width of the drop zone. */\n width: number;\n /** Height of the drop zone. */\n height: number;\n /** Callback when files are dropped. */\n onDrop?: (files: File[]) => void;\n /** Children to render inside the drop zone. */\n children: ReactNode;\n}\n\nexport function DropZone({\n enabled,\n theme,\n width,\n height,\n onDrop,\n children,\n}: DropZoneProps) {\n const [isDragging, setIsDragging] = useState(false);\n const dragCountRef = { current: 0 };\n\n const handleDragEnter = useCallback(\n (e: React.DragEvent) => {\n if (!enabled) return;\n e.preventDefault();\n dragCountRef.current++;\n setIsDragging(true);\n },\n [enabled],\n );\n\n const handleDragLeave = useCallback(\n (e: React.DragEvent) => {\n if (!enabled) return;\n e.preventDefault();\n dragCountRef.current--;\n if (dragCountRef.current <= 0) {\n dragCountRef.current = 0;\n setIsDragging(false);\n }\n },\n [enabled],\n );\n\n const handleDragOver = useCallback(\n (e: React.DragEvent) => {\n if (!enabled) return;\n e.preventDefault();\n e.dataTransfer.dropEffect = \"copy\";\n },\n [enabled],\n );\n\n const handleDrop = useCallback(\n (e: React.DragEvent) => {\n if (!enabled) return;\n e.preventDefault();\n dragCountRef.current = 0;\n setIsDragging(false);\n const files = Array.from(e.dataTransfer.files);\n if (files.length > 0) {\n onDrop?.(files);\n }\n },\n [enabled, onDrop],\n );\n\n return (\n <div\n style={{ position: \"relative\", width, height }}\n onDragEnter={handleDragEnter}\n onDragLeave={handleDragLeave}\n onDragOver={handleDragOver}\n onDrop={handleDrop}\n >\n {children}\n {isDragging && (\n <div\n data-testid=\"dropzone-overlay\"\n style={{\n position: \"absolute\",\n inset: 0,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n background:\n theme === \"dark\"\n ? \"rgba(30, 58, 138, 0.6)\"\n : \"rgba(59, 130, 246, 0.15)\",\n border: `2px dashed ${theme === \"dark\" ? \"#60a5fa\" : \"#3b82f6\"}`,\n borderRadius: 4,\n zIndex: 100,\n pointerEvents: \"none\",\n fontSize: 14,\n fontFamily: \"system-ui, sans-serif\",\n color: theme === \"dark\" ? \"#93c5fd\" : \"#1d4ed8\",\n fontWeight: 500,\n }}\n >\n Drop spectrum files here\n </div>\n )}\n </div>\n );\n}\n","/**\n * Stacked display mode for comparing multiple spectra vertically.\n *\n * Each spectrum gets its own y-axis panel. Shared x-axis at the bottom.\n * Zoom/pan is synchronized across all panels.\n */\n\nimport { useMemo } from \"react\";\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { Spectrum, Margin, Theme } from \"../../types\";\nimport { computeYExtent, createYScale } from \"../../utils/scales\";\nimport { getSpectrumColor, getThemeColors } from \"../../utils/colors\";\nimport { AxisLayer } from \"../AxisLayer/AxisLayer\";\nimport { SpectrumCanvas } from \"../SpectrumCanvas/SpectrumCanvas\";\n\nexport interface StackedViewProps {\n /** Spectra to display. */\n spectra: Spectrum[];\n /** Zoomed x-scale (shared across panels). */\n xScale: ScaleLinear<number, number>;\n /** Full plot width. */\n plotWidth: number;\n /** Full plot height (will be divided among panels). */\n plotHeight: number;\n /** Chart margins. */\n margin: Margin;\n /** Theme. */\n theme: Theme;\n /** Show grid lines. */\n showGrid: boolean;\n /** X-axis label. */\n xLabel: string;\n /** Y-axis label. */\n yLabel: string;\n}\n\n/** Gap between stacked panels in pixels. */\nconst PANEL_GAP = 8;\n\nexport function StackedView({\n spectra,\n xScale,\n plotWidth,\n plotHeight,\n margin,\n theme,\n showGrid,\n xLabel,\n yLabel,\n}: StackedViewProps) {\n const visible = spectra.filter((s) => s.visible !== false);\n const colors = useMemo(() => getThemeColors(theme), [theme]);\n const panelCount = visible.length;\n const totalGap = (panelCount - 1) * PANEL_GAP;\n const panelHeight = Math.max(\n 40,\n Math.floor((plotHeight - totalGap) / Math.max(panelCount, 1)),\n );\n\n return (\n <g className=\"spectraview-stacked\">\n {visible.map((spectrum, i) => {\n const yOffset = i * (panelHeight + PANEL_GAP);\n const yExtent = computeYExtent([spectrum]);\n const yScale = createYScale(\n yExtent,\n panelHeight + margin.top + margin.bottom,\n { ...margin, top: 0, bottom: 0 },\n );\n const color = spectrum.color ?? getSpectrumColor(i);\n const coloredSpectrum = { ...spectrum, color };\n\n return (\n <g key={spectrum.id} transform={`translate(0, ${yOffset})`}>\n {/* Panel background */}\n <rect\n x={0}\n y={0}\n width={plotWidth}\n height={panelHeight}\n fill=\"transparent\"\n stroke={colors.gridColor}\n strokeWidth={0.5}\n rx={2}\n />\n\n {/* Axes */}\n <AxisLayer\n xScale={xScale}\n yScale={yScale}\n width={plotWidth}\n height={panelHeight}\n xLabel={i === panelCount - 1 ? xLabel : \"\"}\n yLabel={yLabel}\n showGrid={showGrid}\n colors={colors}\n />\n\n {/* Spectrum label */}\n <text\n x={4}\n y={14}\n fill={color}\n fontSize={11}\n fontFamily=\"system-ui, sans-serif\"\n fontWeight={500}\n >\n {spectrum.label}\n </text>\n\n {/* Canvas for this panel */}\n <foreignObject x={0} y={0} width={plotWidth} height={panelHeight}>\n <SpectrumCanvas\n spectra={[coloredSpectrum]}\n xScale={xScale}\n yScale={yScale}\n width={plotWidth}\n height={panelHeight}\n />\n </foreignObject>\n </g>\n );\n })}\n </g>\n );\n}\n","/**\n * Hook for interactive region selection via Shift+drag.\n *\n * Tracks mouse drag state when Shift is held, converting pixel\n * coordinates to data-space values using the provided x-scale.\n */\n\nimport { useCallback, useRef, useState } from \"react\";\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { Region } from \"../types\";\n\nexport interface UseRegionSelectOptions {\n /** Whether region selection is enabled. */\n enabled: boolean;\n /** X-axis scale for pixel-to-data conversion. */\n xScale: ScaleLinear<number, number>;\n /** Callback when a region is created. */\n onRegionSelect?: (region: Region) => void;\n}\n\nexport interface UseRegionSelectReturn {\n /** Pending region being dragged (null when not dragging). */\n pendingRegion: Region | null;\n /** Mouse down handler — call on the interaction rect. */\n handleMouseDown: (event: React.MouseEvent<SVGRectElement>) => void;\n /** Mouse move handler — call on the interaction rect. */\n handleMouseMove: (event: React.MouseEvent<SVGRectElement>) => void;\n /** Mouse up handler — call on the interaction rect. */\n handleMouseUp: () => void;\n}\n\nexport function useRegionSelect(\n options: UseRegionSelectOptions,\n): UseRegionSelectReturn {\n const { enabled, xScale, onRegionSelect } = options;\n const [pendingRegion, setPendingRegion] = useState<Region | null>(null);\n const dragStartRef = useRef<number | null>(null);\n\n const handleMouseDown = useCallback(\n (event: React.MouseEvent<SVGRectElement>) => {\n if (!enabled || !event.shiftKey) return;\n event.preventDefault();\n const rect = event.currentTarget.getBoundingClientRect();\n const px = event.clientX - rect.left;\n const dataX = xScale.invert(px);\n dragStartRef.current = dataX;\n setPendingRegion({ xStart: dataX, xEnd: dataX });\n },\n [enabled, xScale],\n );\n\n const handleMouseMove = useCallback(\n (event: React.MouseEvent<SVGRectElement>) => {\n if (dragStartRef.current === null) return;\n const rect = event.currentTarget.getBoundingClientRect();\n const px = event.clientX - rect.left;\n const dataX = xScale.invert(px);\n const start = dragStartRef.current;\n setPendingRegion({\n xStart: Math.min(start, dataX),\n xEnd: Math.max(start, dataX),\n });\n },\n [xScale],\n );\n\n const handleMouseUp = useCallback(() => {\n if (dragStartRef.current === null || !pendingRegion) return;\n const width = Math.abs(pendingRegion.xEnd - pendingRegion.xStart);\n if (width > 0) {\n onRegionSelect?.(pendingRegion);\n }\n dragStartRef.current = null;\n setPendingRegion(null);\n }, [pendingRegion, onRegionSelect]);\n\n return { pendingRegion, handleMouseDown, handleMouseMove, handleMouseUp };\n}\n","/**\n * Hook for responsive sizing via ResizeObserver.\n *\n * Tracks the width (and optionally height) of a container element,\n * enabling the chart to auto-size to its parent.\n */\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nexport interface ResizeObserverSize {\n width: number;\n height: number;\n}\n\nexport function useResizeObserver(): {\n ref: React.RefCallback<HTMLElement>;\n size: ResizeObserverSize | null;\n} {\n const [size, setSize] = useState<ResizeObserverSize | null>(null);\n const observerRef = useRef<ResizeObserver | null>(null);\n const elementRef = useRef<HTMLElement | null>(null);\n\n const ref = useCallback((node: HTMLElement | null) => {\n // Cleanup previous observer\n if (observerRef.current) {\n observerRef.current.disconnect();\n observerRef.current = null;\n }\n\n elementRef.current = node;\n\n if (!node) return;\n\n const observer = new ResizeObserver((entries) => {\n const entry = entries[0];\n if (!entry) return;\n const { width, height } = entry.contentRect;\n setSize({ width: Math.round(width), height: Math.round(height) });\n });\n\n observer.observe(node);\n observerRef.current = observer;\n\n // Set initial size\n const { width, height } = node.getBoundingClientRect();\n setSize({ width: Math.round(width), height: Math.round(height) });\n }, []);\n\n useEffect(() => {\n return () => {\n observerRef.current?.disconnect();\n };\n }, []);\n\n return { ref, size };\n}\n","/**\n * Hook for keyboard navigation within the spectrum viewer.\n *\n * Handles arrow keys for panning, +/- for zoom, Escape for reset.\n */\n\nimport { useCallback } from \"react\";\n\nexport interface UseKeyboardNavigationOptions {\n /** Zoom in handler. */\n onZoomIn: () => void;\n /** Zoom out handler. */\n onZoomOut: () => void;\n /** Reset zoom handler. */\n onReset: () => void;\n /** Whether keyboard navigation is enabled. */\n enabled?: boolean;\n}\n\nexport function useKeyboardNavigation(\n options: UseKeyboardNavigationOptions,\n): (event: React.KeyboardEvent) => void {\n const { onZoomIn, onZoomOut, onReset, enabled = true } = options;\n\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent) => {\n if (!enabled) return;\n\n switch (event.key) {\n case \"+\":\n case \"=\":\n event.preventDefault();\n onZoomIn();\n break;\n case \"-\":\n event.preventDefault();\n onZoomOut();\n break;\n case \"Escape\":\n event.preventDefault();\n onReset();\n break;\n }\n },\n [enabled, onZoomIn, onZoomOut, onReset],\n );\n\n return handleKeyDown;\n}\n","/**\n * Accessibility utilities for SpectraView.\n *\n * Provides helpers for reduced motion detection, ARIA label generation,\n * and keyboard navigation constants.\n */\n\n/** Check if the user prefers reduced motion. */\nexport function prefersReducedMotion(): boolean {\n if (typeof window === \"undefined\") return false;\n return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n}\n\n/** Generate an accessible description for a spectrum chart. */\nexport function generateChartDescription(\n spectrumCount: number,\n xLabel: string,\n yLabel: string,\n): string {\n if (spectrumCount === 0) return \"Empty spectrum viewer\";\n const plural = spectrumCount === 1 ? \"spectrum\" : \"spectra\";\n return `Interactive spectrum viewer showing ${spectrumCount} ${plural}. X-axis: ${xLabel}. Y-axis: ${yLabel}. Use arrow keys to pan, +/- to zoom, Escape to reset.`;\n}\n\n/** Keyboard shortcut definitions. */\nexport const KEYBOARD_SHORTCUTS = {\n PAN_LEFT: \"ArrowLeft\",\n PAN_RIGHT: \"ArrowRight\",\n PAN_UP: \"ArrowUp\",\n PAN_DOWN: \"ArrowDown\",\n ZOOM_IN: \"+\",\n ZOOM_IN_ALT: \"=\",\n ZOOM_OUT: \"-\",\n RESET: \"Escape\",\n NEXT_PEAK: \"Tab\",\n PREV_PEAK: \"Shift+Tab\",\n} as const;\n","/**\n * Minimap — A small overview component showing the full spectrum\n * with a highlighted viewport rectangle indicating the current\n * zoom region.\n *\n * Renders using Canvas for the spectrum line and SVG for the\n * viewport overlay.\n */\n\nimport { memo, useEffect, useRef, useMemo } from \"react\";\nimport { scaleLinear } from \"d3-scale\";\nimport type { Spectrum, Theme } from \"../../types\";\nimport { getThemeColors } from \"../../utils/colors\";\nimport { getSpectrumColor } from \"../../utils/colors\";\n\nexport interface MinimapProps {\n /** Spectra to render in the minimap. */\n spectra: Spectrum[];\n /** Full X extent [min, max] of the data. */\n xExtent: [number, number];\n /** Full Y extent [min, max] of the data. */\n yExtent: [number, number];\n /** Currently visible X domain from the zoomed view. */\n visibleXDomain: [number, number];\n /** Currently visible Y domain from the zoomed view (reserved for future use). */\n visibleYDomain?: [number, number];\n /** Minimap width in pixels. Defaults to 200. */\n width?: number;\n /** Minimap height in pixels. Defaults to 50. */\n height?: number;\n /** Theme. */\n theme?: Theme;\n /** Whether the view is currently zoomed. */\n isZoomed?: boolean;\n}\n\nexport const Minimap = memo(function Minimap({\n spectra,\n xExtent,\n yExtent,\n visibleXDomain,\n width = 200,\n height = 50,\n theme = \"light\",\n isZoomed = false,\n}: MinimapProps) {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const colors = useMemo(() => getThemeColors(theme), [theme]);\n\n // Full-range scales for the minimap\n const xScale = useMemo(\n () => scaleLinear().domain(xExtent).range([0, width]),\n [xExtent, width],\n );\n const yScale = useMemo(\n () => scaleLinear().domain(yExtent).range([height - 2, 2]),\n [yExtent, height],\n );\n\n // Draw spectra on canvas\n useEffect(() => {\n const ctx = canvasRef.current?.getContext(\"2d\");\n if (!ctx) return;\n\n ctx.clearRect(0, 0, width, height);\n\n for (let s = 0; s < spectra.length; s++) {\n const spectrum = spectra[s];\n if (spectrum.visible === false) continue;\n const n = Math.min(spectrum.x.length, spectrum.y.length);\n if (n < 2) continue;\n\n const color = spectrum.color ?? getSpectrumColor(s);\n ctx.beginPath();\n ctx.strokeStyle = color;\n ctx.lineWidth = 1;\n ctx.globalAlpha = 0.7;\n\n // Decimate for minimap: use every Nth point\n const step = Math.max(1, Math.floor(n / width));\n let started = false;\n\n for (let i = 0; i < n; i += step) {\n const px = xScale(spectrum.x[i] as number);\n const py = yScale(spectrum.y[i] as number);\n if (!started) {\n ctx.moveTo(px, py);\n started = true;\n } else {\n ctx.lineTo(px, py);\n }\n }\n\n ctx.stroke();\n }\n }, [spectra, xScale, yScale, width, height]);\n\n // Compute viewport rectangle position\n const vpLeft = xScale(Math.min(visibleXDomain[0], visibleXDomain[1]));\n const vpRight = xScale(Math.max(visibleXDomain[0], visibleXDomain[1]));\n const vpWidth = Math.max(vpRight - vpLeft, 2);\n\n return (\n <div\n className=\"spectraview-minimap\"\n style={{\n position: \"relative\",\n width,\n height,\n border: `1px solid ${colors.gridColor}`,\n borderRadius: 3,\n overflow: \"hidden\",\n background: colors.background,\n }}\n >\n <canvas\n ref={canvasRef}\n width={width}\n height={height}\n style={{ position: \"absolute\", top: 0, left: 0 }}\n />\n {isZoomed && (\n <svg\n width={width}\n height={height}\n style={{ position: \"absolute\", top: 0, left: 0 }}\n >\n {/* Dim outside viewport */}\n <rect\n x={0}\n y={0}\n width={vpLeft}\n height={height}\n fill={colors.background}\n opacity={0.6}\n />\n <rect\n x={vpLeft + vpWidth}\n y={0}\n width={width - vpLeft - vpWidth}\n height={height}\n fill={colors.background}\n opacity={0.6}\n />\n {/* Viewport rectangle */}\n <rect\n x={vpLeft}\n y={0}\n width={vpWidth}\n height={height}\n fill=\"none\"\n stroke={theme === \"dark\" ? \"#60a5fa\" : \"#3b82f6\"}\n strokeWidth={1.5}\n rx={1}\n />\n </svg>\n )}\n </div>\n );\n});\n","/**\n * Tooltip — Enhanced multi-spectrum data readout on hover.\n *\n * Shows values from all visible spectra at the current cursor X position,\n * with colored indicators matching each spectrum's line color.\n * Optionally shows the nearest peak annotation.\n */\n\nimport { memo, useMemo } from \"react\";\nimport type { Spectrum, Peak } from \"../../types\";\nimport type { ThemeColors } from \"../../utils/colors\";\nimport { getSpectrumColor } from \"../../utils/colors\";\nimport { binarySearchClosest } from \"../../utils/snap\";\n\nexport interface TooltipData {\n /** Cursor pixel position. */\n px: number;\n py: number;\n /** Cursor data-space position. */\n dataX: number;\n dataY: number;\n}\n\nexport interface TooltipProps {\n /** Tooltip position, or null when hidden. */\n data: TooltipData | null;\n /** Spectra for multi-value readout. */\n spectra: Spectrum[];\n /** Peaks for nearest-peak indicator. */\n peaks?: Peak[];\n /** Plot area width. */\n plotWidth: number;\n /** Plot area height. */\n plotHeight: number;\n /** Theme colors. */\n colors: ThemeColors;\n /** Number format for values. Defaults to \"auto\". */\n numberFormat?: \"auto\" | \"fixed2\" | \"fixed4\" | \"scientific\";\n}\n\nfunction formatNumber(v: number, format: string): string {\n switch (format) {\n case \"fixed2\":\n return v.toFixed(2);\n case \"fixed4\":\n return v.toFixed(4);\n case \"scientific\":\n return v.toExponential(2);\n default: // auto\n if (Math.abs(v) >= 100) return Math.round(v).toString();\n if (Math.abs(v) >= 1) return v.toFixed(2);\n if (Math.abs(v) >= 0.01) return v.toFixed(4);\n return v.toExponential(2);\n }\n}\n\nexport const Tooltip = memo(function Tooltip({\n data,\n spectra,\n peaks = [],\n plotWidth,\n plotHeight,\n colors,\n numberFormat = \"auto\",\n}: TooltipProps) {\n if (!data) return null;\n\n // Find Y values for each visible spectrum at cursor X\n const entries = useMemo(() => {\n if (!data) return [];\n return spectra\n .filter((s) => s.visible !== false)\n .map((spectrum, i) => {\n const n = Math.min(spectrum.x.length, spectrum.y.length);\n if (n < 1) return null;\n const idx = binarySearchClosest(spectrum.x, data.dataX, n);\n if (idx < 0) return null;\n return {\n label: spectrum.label,\n color: spectrum.color ?? getSpectrumColor(i),\n value: spectrum.y[idx] as number,\n x: spectrum.x[idx] as number,\n };\n })\n .filter(Boolean) as Array<{\n label: string;\n color: string;\n value: number;\n x: number;\n }>;\n }, [data?.dataX, spectra]);\n\n // Find nearest peak\n const nearestPeak = useMemo(() => {\n if (!data || peaks.length === 0) return null;\n let best: Peak | null = null;\n let bestDist = Infinity;\n for (const peak of peaks) {\n const dist = Math.abs(peak.x - data.dataX);\n if (dist < bestDist) {\n bestDist = dist;\n best = peak;\n }\n }\n return best;\n }, [data?.dataX, peaks]);\n\n const lineHeight = 16;\n const headerHeight = 18;\n const peakLineHeight = nearestPeak ? lineHeight : 0;\n const tooltipHeight = headerHeight + entries.length * lineHeight + peakLineHeight + 8;\n const tooltipWidth = 160;\n\n // Position tooltip to avoid clipping\n let tx = data.px + 15;\n let ty = data.py - tooltipHeight / 2;\n if (tx + tooltipWidth > plotWidth) tx = data.px - tooltipWidth - 15;\n if (ty < 0) ty = 4;\n if (ty + tooltipHeight > plotHeight) ty = plotHeight - tooltipHeight - 4;\n\n return (\n <g\n className=\"spectraview-tooltip\"\n transform={`translate(${tx}, ${ty})`}\n pointerEvents=\"none\"\n >\n {/* Background */}\n <rect\n x={0}\n y={0}\n width={tooltipWidth}\n height={tooltipHeight}\n rx={4}\n fill={colors.tooltipBg}\n stroke={colors.tooltipBorder}\n strokeWidth={0.5}\n opacity={0.95}\n />\n\n {/* Header: X value */}\n <text\n x={8}\n y={14}\n fill={colors.tooltipText}\n fontSize={10}\n fontFamily=\"monospace\"\n fontWeight={600}\n >\n x = {formatNumber(data.dataX, numberFormat)}\n </text>\n\n {/* Spectrum values */}\n {entries.map((entry, i) => (\n <g key={entry.label} transform={`translate(0, ${headerHeight + i * lineHeight})`}>\n <circle cx={12} cy={8} r={3} fill={entry.color} />\n <text\n x={20}\n y={11}\n fill={colors.tooltipText}\n fontSize={9}\n fontFamily=\"monospace\"\n >\n {entry.label.slice(0, 10)}: {formatNumber(entry.value, numberFormat)}\n </text>\n </g>\n ))}\n\n {/* Nearest peak */}\n {nearestPeak && (\n <text\n x={8}\n y={headerHeight + entries.length * lineHeight + 12}\n fill={colors.labelColor}\n fontSize={9}\n fontFamily=\"monospace\"\n fontStyle=\"italic\"\n >\n Peak: {nearestPeak.label ?? formatNumber(nearestPeak.x, numberFormat)}\n </text>\n )}\n </g>\n );\n});\n","/**\n * Hook for automatic peak detection in spectral data.\n *\n * Wraps the peak detection algorithm with React state management\n * and recalculates when spectra or options change.\n */\n\nimport { useMemo } from \"react\";\nimport type { Spectrum, Peak } from \"../types\";\nimport { detectPeaks, type PeakDetectionOptions } from \"../utils/peaks\";\n\nexport interface UsePeakPickingOptions extends PeakDetectionOptions {\n /** Whether peak picking is enabled. */\n enabled?: boolean;\n /** Only detect peaks for these spectrum IDs (all if not specified). */\n spectrumIds?: string[];\n}\n\n/**\n * Automatically detect peaks across one or more spectra.\n *\n * @param spectra - Array of spectra to analyze\n * @param options - Peak detection configuration\n * @returns Array of detected peaks with associated spectrum IDs\n */\nexport function usePeakPicking(\n spectra: Spectrum[],\n options: UsePeakPickingOptions = {},\n): Peak[] {\n const {\n enabled = true,\n spectrumIds,\n prominence,\n minDistance,\n maxPeaks,\n } = options;\n\n return useMemo(() => {\n if (!enabled) return [];\n\n const targetSpectra = spectrumIds\n ? spectra.filter((s) => spectrumIds.includes(s.id))\n : spectra;\n\n const allPeaks: Peak[] = [];\n\n for (const spectrum of targetSpectra) {\n if (spectrum.visible === false) continue;\n\n const peaks = detectPeaks(spectrum.x, spectrum.y, {\n prominence,\n minDistance,\n maxPeaks,\n });\n\n for (const peak of peaks) {\n allPeaks.push({\n ...peak,\n spectrumId: spectrum.id,\n });\n }\n }\n\n return allPeaks;\n }, [spectra, enabled, spectrumIds, prominence, minDistance, maxPeaks]);\n}\n","/**\n * Peak detection for spectral data.\n *\n * Uses a simple local-maxima algorithm with prominence filtering,\n * suitable for identifying peaks in IR, Raman, and NIR spectra.\n */\n\nimport type { Peak } from \"../types\";\n\n/** Default minimum prominence for peak detection. */\nconst DEFAULT_PROMINENCE = 0.01;\n\n/** Default minimum distance between peaks (in number of points). */\nconst DEFAULT_MIN_DISTANCE = 5;\n\nexport interface PeakDetectionOptions {\n /** Minimum prominence relative to the signal range. */\n prominence?: number;\n /** Minimum distance between peaks in data points. */\n minDistance?: number;\n /** Maximum number of peaks to return (sorted by prominence). */\n maxPeaks?: number;\n}\n\n/**\n * Detect peaks in a 1D signal using local maxima with prominence filtering.\n *\n * @param x - X-axis values (wavenumbers, wavelengths, etc.)\n * @param y - Y-axis values (absorbance, intensity, etc.)\n * @param options - Detection parameters\n * @returns Array of detected peaks sorted by x position\n */\nexport function detectPeaks(\n x: Float64Array | number[],\n y: Float64Array | number[],\n options: PeakDetectionOptions = {},\n): Peak[] {\n const {\n prominence = DEFAULT_PROMINENCE,\n minDistance = DEFAULT_MIN_DISTANCE,\n maxPeaks,\n } = options;\n\n if (x.length < 3 || y.length < 3) return [];\n\n // Find the signal range for prominence scaling\n let yMin = Infinity;\n let yMax = -Infinity;\n for (let i = 0; i < y.length; i++) {\n if (y[i] < yMin) yMin = y[i];\n if (y[i] > yMax) yMax = y[i];\n }\n const signalRange = yMax - yMin;\n if (signalRange === 0) return [];\n\n const absProminence = prominence * signalRange;\n\n // Find local maxima\n const candidates: { index: number; prom: number }[] = [];\n for (let i = 1; i < y.length - 1; i++) {\n if (y[i] > y[i - 1] && y[i] > y[i + 1]) {\n // Compute prominence: min drop to each side before a higher peak\n const leftMin = findMinBefore(y, i);\n const rightMin = findMinAfter(y, i);\n const prom = y[i] - Math.max(leftMin, rightMin);\n\n if (prom >= absProminence) {\n candidates.push({ index: i, prom });\n }\n }\n }\n\n // Sort by prominence (highest first) for distance filtering\n candidates.sort((a, b) => b.prom - a.prom);\n\n // Filter by minimum distance (keep most prominent first)\n const kept: { index: number; prom: number }[] = [];\n for (const c of candidates) {\n const tooClose = kept.some(\n (k) => Math.abs(k.index - c.index) < minDistance,\n );\n if (!tooClose) {\n kept.push(c);\n }\n }\n\n // Apply max peaks limit\n const selected = maxPeaks ? kept.slice(0, maxPeaks) : kept;\n\n // Convert to Peak objects, sorted by x position\n return selected\n .map((c) => ({\n x: x[c.index] as number,\n y: y[c.index] as number,\n label: formatWavenumber(x[c.index] as number),\n }))\n .sort((a, b) => a.x - b.x);\n}\n\n/**\n * Find the minimum value in y before index i, stopping at a higher peak.\n */\nfunction findMinBefore(y: Float64Array | number[], i: number): number {\n let min = y[i] as number;\n for (let j = i - 1; j >= 0; j--) {\n if (y[j] > y[i]) break;\n if ((y[j] as number) < min) min = y[j] as number;\n }\n return min;\n}\n\n/**\n * Find the minimum value in y after index i, stopping at a higher peak.\n */\nfunction findMinAfter(y: Float64Array | number[], i: number): number {\n let min = y[i] as number;\n for (let j = i + 1; j < y.length; j++) {\n if (y[j] > y[i]) break;\n if ((y[j] as number) < min) min = y[j] as number;\n }\n return min;\n}\n\n/**\n * Format a wavenumber value for display as a peak label.\n */\nfunction formatWavenumber(value: number): string {\n return Math.round(value).toString();\n}\n","/**\n * Hook for managing spectrum data loading and state.\n *\n * Handles file loading (drag-and-drop, file input), parsing,\n * and managing the collection of loaded spectra.\n */\n\nimport { useCallback, useState } from \"react\";\nimport type { Spectrum } from \"../types\";\nimport { parseCsv } from \"../parsers/csv\";\nimport { parseJson } from \"../parsers/json\";\nimport { parseJcamp } from \"../parsers/jcamp\";\n\nexport interface UseSpectrumDataReturn {\n /** Currently loaded spectra. */\n spectra: Spectrum[];\n /** Whether a file is currently being loaded. */\n loading: boolean;\n /** Last error message, if any. */\n error: string | null;\n /** Load spectra from a File object (detects format from extension). */\n loadFile: (file: File) => Promise<void>;\n /** Load spectra from a raw text string with explicit format. */\n loadText: (text: string, format: \"jcamp\" | \"csv\" | \"json\") => Promise<void>;\n /** Add a spectrum directly. */\n addSpectrum: (spectrum: Spectrum) => void;\n /** Remove a spectrum by ID. */\n removeSpectrum: (id: string) => void;\n /** Toggle a spectrum's visibility. */\n toggleVisibility: (id: string) => void;\n /** Clear all loaded spectra. */\n clear: () => void;\n}\n\n/**\n * Detect file format from file extension.\n */\nfunction detectFormat(filename: string): \"jcamp\" | \"csv\" | \"json\" | null {\n const ext = filename.toLowerCase().split(\".\").pop();\n switch (ext) {\n case \"dx\":\n case \"jdx\":\n case \"jcamp\":\n return \"jcamp\";\n case \"csv\":\n case \"tsv\":\n case \"txt\":\n return \"csv\";\n case \"json\":\n return \"json\";\n default:\n return null;\n }\n}\n\n/**\n * Hook for loading and managing spectral data.\n */\nexport function useSpectrumData(\n initialSpectra: Spectrum[] = [],\n): UseSpectrumDataReturn {\n const [spectra, setSpectra] = useState<Spectrum[]>(initialSpectra);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const loadText = useCallback(\n async (text: string, format: \"jcamp\" | \"csv\" | \"json\") => {\n setLoading(true);\n setError(null);\n\n try {\n let parsed: Spectrum[];\n\n switch (format) {\n case \"jcamp\":\n parsed = await parseJcamp(text);\n break;\n case \"csv\":\n parsed = [parseCsv(text)];\n break;\n case \"json\":\n parsed = parseJson(text);\n break;\n }\n\n setSpectra((prev) => [...prev, ...parsed]);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Failed to parse file\";\n setError(message);\n } finally {\n setLoading(false);\n }\n },\n [],\n );\n\n const loadFile = useCallback(\n async (file: File) => {\n const format = detectFormat(file.name);\n if (!format) {\n setError(`Unsupported file format: ${file.name}`);\n return;\n }\n\n const text = await file.text();\n await loadText(text, format);\n },\n [loadText],\n );\n\n const addSpectrum = useCallback((spectrum: Spectrum) => {\n setSpectra((prev) => [...prev, spectrum]);\n }, []);\n\n const removeSpectrum = useCallback((id: string) => {\n setSpectra((prev) => prev.filter((s) => s.id !== id));\n }, []);\n\n const toggleVisibility = useCallback((id: string) => {\n setSpectra((prev) =>\n prev.map((s) =>\n s.id === id ? { ...s, visible: s.visible === false ? true : false } : s,\n ),\n );\n }, []);\n\n const clear = useCallback(() => {\n setSpectra([]);\n setError(null);\n }, []);\n\n return {\n spectra,\n loading,\n error,\n loadFile,\n loadText,\n addSpectrum,\n removeSpectrum,\n toggleVisibility,\n clear,\n };\n}\n","/**\n * CSV/TSV parser for spectral data.\n *\n * Handles comma, tab, and semicolon delimiters with automatic detection.\n * Supports files with or without header rows.\n */\n\nimport type { Spectrum } from \"../types\";\n\n/** Auto-incrementing ID counter for unique spectrum IDs. */\nlet idCounter = 0;\n\nexport interface CsvParseOptions {\n /** Column delimiter (auto-detected if not provided). */\n delimiter?: string;\n /** Zero-based index of the x-value column. */\n xColumn?: number;\n /** Zero-based index of the y-value column. */\n yColumn?: number;\n /** Whether the first row is a header. */\n hasHeader?: boolean;\n /** Label for the parsed spectrum. */\n label?: string;\n}\n\n/** Delimiters to try during auto-detection. */\nconst DELIMITER_CANDIDATES = [\"\\t\", \",\", \";\", \" \"] as const;\n\n/**\n * Auto-detect the delimiter used in a CSV text.\n *\n * Counts occurrences of each candidate delimiter in the first 5 lines\n * and picks the one that appears most consistently.\n */\nfunction detectDelimiter(text: string): string {\n const lines = text.trim().split(/\\r?\\n/).slice(0, 5);\n let bestDelimiter = \",\";\n let bestScore = 0;\n\n for (const d of DELIMITER_CANDIDATES) {\n const counts = lines.map((line) => line.split(d).length - 1);\n const minCount = Math.min(...counts);\n // A good delimiter appears consistently across all lines\n if (minCount > 0 && minCount >= bestScore) {\n const consistent = counts.every((c) => c === counts[0]);\n if (consistent || minCount > bestScore) {\n bestScore = minCount;\n bestDelimiter = d;\n }\n }\n }\n\n return bestDelimiter;\n}\n\n/**\n * Parse a CSV/TSV string into a Spectrum object.\n *\n * @param text - Raw CSV/TSV text content\n * @param options - Parsing configuration\n * @returns Parsed Spectrum\n * @throws Error if the data cannot be parsed\n */\nexport function parseCsv(text: string, options: CsvParseOptions = {}): Spectrum {\n const {\n xColumn = 0,\n yColumn = 1,\n hasHeader = true,\n label = \"CSV Spectrum\",\n } = options;\n\n const delimiter = options.delimiter ?? detectDelimiter(text);\n const lines = text.trim().split(/\\r?\\n/);\n\n if (lines.length < 2) {\n throw new Error(\"CSV file must contain at least 2 lines\");\n }\n\n let headerLabel = label;\n let startIndex = 0;\n\n if (hasHeader) {\n const headers = lines[0].split(delimiter).map((h) => h.trim());\n // Only use header as label if no explicit label was provided\n if (!options.label && headers[yColumn]) {\n headerLabel = headers[yColumn];\n }\n startIndex = 1;\n }\n\n const xValues: number[] = [];\n const yValues: number[] = [];\n\n for (let i = startIndex; i < lines.length; i++) {\n const line = lines[i].trim();\n if (line === \"\" || line.startsWith(\"#\")) continue;\n\n const parts = line.split(delimiter);\n const xVal = parseFloat(parts[xColumn]);\n const yVal = parseFloat(parts[yColumn]);\n\n if (!isNaN(xVal) && !isNaN(yVal)) {\n xValues.push(xVal);\n yValues.push(yVal);\n }\n }\n\n if (xValues.length === 0) {\n throw new Error(\"No valid numeric data found in CSV\");\n }\n\n return {\n id: `csv-${++idCounter}`,\n label: headerLabel,\n x: new Float64Array(xValues),\n y: new Float64Array(yValues),\n };\n}\n\n/**\n * Parse a CSV string containing multiple y-columns into multiple spectra.\n *\n * The first column is treated as x-values, and each subsequent column\n * becomes a separate spectrum.\n */\nexport function parseCsvMulti(\n text: string,\n options: Omit<CsvParseOptions, \"xColumn\" | \"yColumn\"> = {},\n): Spectrum[] {\n const { hasHeader = true, label } = options;\n const delimiter = options.delimiter ?? detectDelimiter(text);\n const lines = text.trim().split(/\\r?\\n/);\n\n if (lines.length < 2) {\n throw new Error(\"CSV file must contain at least 2 lines\");\n }\n\n const firstDataLine = lines[hasHeader ? 1 : 0];\n const numColumns = firstDataLine.split(delimiter).length;\n\n if (numColumns < 2) {\n throw new Error(\"CSV must have at least 2 columns (x + y)\");\n }\n\n let headers: string[] | undefined;\n let startIndex = 0;\n\n if (hasHeader) {\n headers = lines[0].split(delimiter).map((h) => h.trim());\n startIndex = 1;\n }\n\n const xValues: number[] = [];\n const yArrays: number[][] = Array.from({ length: numColumns - 1 }, () => []);\n\n for (let i = startIndex; i < lines.length; i++) {\n const line = lines[i].trim();\n if (line === \"\" || line.startsWith(\"#\")) continue;\n\n const parts = line.split(delimiter);\n const xVal = parseFloat(parts[0]);\n if (isNaN(xVal)) continue;\n\n xValues.push(xVal);\n for (let col = 1; col < numColumns; col++) {\n const yVal = parseFloat(parts[col]);\n yArrays[col - 1].push(isNaN(yVal) ? 0 : yVal);\n }\n }\n\n const xArray = new Float64Array(xValues);\n\n return yArrays.map((yArr, i) => ({\n id: `csv-${++idCounter}`,\n label: label ?? headers?.[i + 1] ?? `Spectrum ${i + 1}`,\n x: xArray,\n y: new Float64Array(yArr),\n }));\n}\n","/**\n * JSON parser for spectral data.\n *\n * Supports multiple JSON formats commonly used for spectral data exchange.\n */\n\nimport type { Spectrum, SpectrumType } from \"../types\";\n\n/** Auto-incrementing ID counter for unique spectrum IDs. */\nlet idCounter = 0;\n\n/**\n * JSON spectrum format: object with x and y arrays.\n *\n * Accepts objects like:\n * ```json\n * {\n * \"label\": \"My Spectrum\",\n * \"x\": [4000, 3999, ...],\n * \"y\": [0.1, 0.12, ...],\n * \"xUnit\": \"cm⁻¹\",\n * \"yUnit\": \"Absorbance\"\n * }\n * ```\n *\n * Also accepts arrays of such objects for multi-spectrum data.\n */\ninterface JsonSpectrumInput {\n label?: string;\n title?: string;\n name?: string;\n x: number[];\n y: number[];\n wavenumbers?: number[];\n wavelengths?: number[];\n intensities?: number[];\n absorbance?: number[];\n xUnit?: string;\n yUnit?: string;\n type?: SpectrumType;\n meta?: Record<string, string | number>;\n}\n\n/**\n * Parse a JSON string into one or more Spectrum objects.\n *\n * Handles both single spectrum objects and arrays of spectra.\n * Supports flexible key names (x/wavenumbers, y/intensities, etc.).\n *\n * @param text - Raw JSON string\n * @returns Array of parsed Spectrum objects\n * @throws Error if the JSON cannot be parsed or has invalid structure\n */\nexport function parseJson(text: string): Spectrum[] {\n let data: unknown;\n try {\n data = JSON.parse(text);\n } catch {\n throw new Error(\"Invalid JSON: failed to parse input\");\n }\n\n if (Array.isArray(data)) {\n return data.map((item, i) => parseSingleJson(item as JsonSpectrumInput, i));\n }\n\n if (typeof data === \"object\" && data !== null) {\n // Check if it's a wrapper object with a \"spectra\" array\n const obj = data as Record<string, unknown>;\n if (Array.isArray(obj.spectra)) {\n return (obj.spectra as JsonSpectrumInput[]).map((item, i) =>\n parseSingleJson(item, i),\n );\n }\n return [parseSingleJson(data as JsonSpectrumInput, 0)];\n }\n\n throw new Error(\"Invalid JSON structure: expected an object or array\");\n}\n\n/**\n * Parse a single JSON object into a Spectrum.\n */\nfunction parseSingleJson(input: JsonSpectrumInput, index: number): Spectrum {\n // Resolve x values from various key names\n const xRaw = input.x ?? input.wavenumbers ?? input.wavelengths;\n if (!xRaw || !Array.isArray(xRaw)) {\n throw new Error(\n `Spectrum ${index}: missing x-axis data (expected \"x\", \"wavenumbers\", or \"wavelengths\")`,\n );\n }\n\n // Resolve y values from various key names\n const yRaw = input.y ?? input.intensities ?? input.absorbance;\n if (!yRaw || !Array.isArray(yRaw)) {\n throw new Error(\n `Spectrum ${index}: missing y-axis data (expected \"y\", \"intensities\", or \"absorbance\")`,\n );\n }\n\n if (xRaw.length !== yRaw.length) {\n throw new Error(\n `Spectrum ${index}: x and y arrays must have the same length (got ${xRaw.length} and ${yRaw.length})`,\n );\n }\n\n const label = input.label ?? input.title ?? input.name ?? `Spectrum ${index + 1}`;\n\n return {\n id: `json-${++idCounter}`,\n label,\n x: new Float64Array(xRaw),\n y: new Float64Array(yRaw),\n xUnit: input.xUnit,\n yUnit: input.yUnit,\n type: input.type,\n meta: input.meta,\n };\n}\n","/**\n * JCAMP-DX parser for spectral data.\n *\n * Wraps the `jcampconverter` npm package (optional peer dependency)\n * to parse .dx, .jdx, and .jcamp files into Spectrum objects.\n *\n * If jcampconverter is not installed, a lightweight built-in parser\n * handles basic AFFN (ASCII Free Format Numeric) JCAMP-DX files.\n */\n\nimport type { Spectrum, SpectrumType } from \"../types\";\n\n/** Auto-incrementing ID counter for unique spectrum IDs. */\nlet idCounter = 0;\n\n/**\n * Minimal shape of jcampconverter output (to avoid hard dependency).\n */\ninterface JcampResult {\n flatten: Array<{\n spectra: Array<{\n data: Array<{\n x: number[];\n y: number[];\n }>;\n }>;\n info: Record<string, string>;\n }>;\n}\n\n/** Cached reference to jcampconverter (lazy-loaded). */\nlet converterModule: { convert: (text: string, options?: object) => JcampResult } | null =\n null;\nlet converterChecked = false;\n\n/**\n * Attempt to dynamically import jcampconverter.\n *\n * Uses a variable to prevent bundlers from statically analyzing the import,\n * since jcampconverter is an optional peer dependency.\n */\nasync function getConverter(): Promise<typeof converterModule> {\n if (converterChecked) return converterModule;\n converterChecked = true;\n try {\n // Variable prevents Vite/Webpack static import analysis\n const pkg = \"jcampconverter\";\n converterModule = await import(/* @vite-ignore */ pkg);\n } catch {\n converterModule = null;\n }\n return converterModule;\n}\n\n/**\n * Infer spectrum type from JCAMP-DX header metadata.\n */\nfunction inferType(info: Record<string, string>): SpectrumType {\n const dataType = (info[\"DATA TYPE\"] ?? info[\"DATATYPE\"] ?? \"\").toLowerCase();\n if (dataType.includes(\"infrared\") || dataType.includes(\"ir\")) return \"IR\";\n if (dataType.includes(\"raman\")) return \"Raman\";\n if (dataType.includes(\"nir\") || dataType.includes(\"near\")) return \"NIR\";\n if (dataType.includes(\"uv\") || dataType.includes(\"vis\")) return \"UV-Vis\";\n if (dataType.includes(\"fluor\")) return \"fluorescence\";\n return \"other\";\n}\n\n/**\n * Parse a JCAMP-DX string into Spectrum objects.\n *\n * Uses jcampconverter if available, otherwise falls back to the built-in\n * parser for basic AFFN format files.\n *\n * @param text - Raw JCAMP-DX file content\n * @returns Array of parsed Spectrum objects\n */\nexport async function parseJcamp(text: string): Promise<Spectrum[]> {\n const converter = await getConverter();\n if (converter) {\n return parseWithConverter(text, converter);\n }\n return [parseBasicJcamp(text)];\n}\n\n/**\n * Parse using jcampconverter library.\n */\nfunction parseWithConverter(\n text: string,\n converter: NonNullable<typeof converterModule>,\n): Spectrum[] {\n const result = converter.convert(text, { keepRecordsRegExp: /.*/ });\n\n return result.flatten.map((entry, i) => {\n const firstSpectrum = entry.spectra?.[0]?.data?.[0];\n if (!firstSpectrum) {\n throw new Error(`JCAMP block ${i}: no spectral data found`);\n }\n\n return {\n id: `jcamp-${++idCounter}`,\n label: entry.info?.TITLE ?? `Spectrum ${i + 1}`,\n x: new Float64Array(firstSpectrum.x),\n y: new Float64Array(firstSpectrum.y),\n xUnit: entry.info?.XUNITS ?? \"cm⁻¹\",\n yUnit: entry.info?.YUNITS ?? \"Absorbance\",\n type: inferType(entry.info),\n meta: entry.info,\n };\n });\n}\n\n/**\n * Lightweight built-in JCAMP-DX parser for basic AFFN format.\n *\n * Handles the most common case: single-spectrum files with\n * XYDATA=(X++(Y..Y)) in ASCII free-format.\n *\n * This does NOT support compressed formats (SQZ, DIF, DIFDUP, NTUP)\n * or multi-block files. For full support, install jcampconverter.\n */\nfunction parseBasicJcamp(text: string): Spectrum {\n const lines = text.split(/\\r?\\n/);\n const info: Record<string, string> = {};\n const xValues: number[] = [];\n const yValues: number[] = [];\n\n let inData = false;\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Parse labeled data records (##KEY= value)\n if (trimmed.startsWith(\"##\")) {\n const match = trimmed.match(/^##(.+?)=\\s*(.*)$/);\n if (match) {\n const key = match[1].trim().toUpperCase();\n const value = match[2].trim();\n\n if (key === \"XYDATA\" || key === \"XYPOINTS\") {\n inData = true;\n continue;\n }\n if (key === \"END\") {\n inData = false;\n continue;\n }\n\n info[key] = value;\n }\n continue;\n }\n\n // Parse data lines\n if (inData && trimmed !== \"\") {\n const values = trimmed.split(/[\\s,]+/).map(Number);\n if (values.length >= 2 && !values.some(isNaN)) {\n // XYDATA: first value is X, rest are Y values\n const x0 = values[0];\n const firstX = parseFloat(info[\"FIRSTX\"] ?? \"0\");\n const lastX = parseFloat(info[\"LASTX\"] ?? \"0\");\n const npoints = parseInt(info[\"NPOINTS\"] ?? \"0\", 10);\n\n if (npoints > 0 && values.length === 2) {\n // Simple X,Y pair format\n xValues.push(values[0]);\n yValues.push(values[1]);\n } else if (values.length > 1) {\n // X++(Y..Y) format — X is first, rest are Y\n const deltaX =\n npoints > 1 ? (lastX - firstX) / (npoints - 1) : 0;\n for (let j = 1; j < values.length; j++) {\n xValues.push(x0 + (j - 1) * deltaX);\n yValues.push(values[j]);\n }\n }\n }\n }\n }\n\n if (xValues.length === 0) {\n throw new Error(\n \"Failed to parse JCAMP-DX: no data found. Install jcampconverter for full format support.\",\n );\n }\n\n return {\n id: `jcamp-${++idCounter}`,\n label: info[\"TITLE\"] ?? \"JCAMP Spectrum\",\n x: new Float64Array(xValues),\n y: new Float64Array(yValues),\n xUnit: info[\"XUNITS\"] ?? \"cm⁻¹\",\n yUnit: info[\"YUNITS\"] ?? \"Absorbance\",\n type: inferType(info),\n meta: info,\n };\n}\n","/**\n * Hook for exporting the spectrum view as PNG, SVG, or CSV data.\n */\n\nimport { useCallback } from \"react\";\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { Spectrum } from \"../types\";\nimport { generateSvg, downloadSvg } from \"../utils/svg-export\";\n\nexport interface UseExportReturn {\n /** Export the canvas as a PNG data URL. */\n exportPng: (canvas: HTMLCanvasElement, filename?: string) => void;\n /** Export visible spectra as SVG vector. */\n exportSvg: (\n spectra: Spectrum[],\n xScale: ScaleLinear<number, number>,\n yScale: ScaleLinear<number, number>,\n width: number,\n height: number,\n filename?: string,\n ) => void;\n /** Export visible spectra as CSV text. */\n exportCsv: (spectra: Spectrum[], filename?: string) => void;\n /** Export visible spectra as JSON. */\n exportJson: (spectra: Spectrum[], filename?: string) => void;\n}\n\n/**\n * Trigger a file download in the browser.\n */\nfunction downloadBlob(blob: Blob, filename: string): void {\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n}\n\n/**\n * Hook for exporting spectrum data and visualizations.\n */\nexport function useExport(): UseExportReturn {\n const exportPng = useCallback(\n (canvas: HTMLCanvasElement, filename = \"spectrum.png\") => {\n canvas.toBlob((blob) => {\n if (blob) downloadBlob(blob, filename);\n }, \"image/png\");\n },\n [],\n );\n\n const exportCsv = useCallback(\n (spectra: Spectrum[], filename = \"spectra.csv\") => {\n const visible = spectra.filter((s) => s.visible !== false);\n if (visible.length === 0) return;\n\n // Build CSV with shared x-axis or per-spectrum columns\n if (visible.length === 1) {\n const s = visible[0];\n const header = `${s.xUnit ?? \"x\"},${s.yUnit ?? \"y\"}\\n`;\n const rows = Array.from(s.x).map(\n (x, i) => `${x},${s.y[i]}`,\n );\n const csv = header + rows.join(\"\\n\");\n downloadBlob(new Blob([csv], { type: \"text/csv\" }), filename);\n } else {\n // Multi-spectrum: each gets its own x,y column pair\n const maxLen = Math.max(...visible.map((s) => s.x.length));\n const header = visible\n .map((s) => `${s.label}_x,${s.label}_y`)\n .join(\",\");\n const rows: string[] = [];\n for (let i = 0; i < maxLen; i++) {\n const values = visible.map((s) => {\n if (i < s.x.length) return `${s.x[i]},${s.y[i]}`;\n return \",\";\n });\n rows.push(values.join(\",\"));\n }\n const csv = header + \"\\n\" + rows.join(\"\\n\");\n downloadBlob(new Blob([csv], { type: \"text/csv\" }), filename);\n }\n },\n [],\n );\n\n const exportJson = useCallback(\n (spectra: Spectrum[], filename = \"spectra.json\") => {\n const visible = spectra.filter((s) => s.visible !== false);\n const output = visible.map((s) => ({\n label: s.label,\n x: Array.from(s.x),\n y: Array.from(s.y),\n xUnit: s.xUnit,\n yUnit: s.yUnit,\n type: s.type,\n }));\n const json = JSON.stringify(output, null, 2);\n downloadBlob(new Blob([json], { type: \"application/json\" }), filename);\n },\n [],\n );\n\n const exportSvg = useCallback(\n (\n spectra: Spectrum[],\n xScale: ScaleLinear<number, number>,\n yScale: ScaleLinear<number, number>,\n width: number,\n height: number,\n filename = \"spectrum.svg\",\n ) => {\n const svg = generateSvg(spectra, xScale, yScale, { width, height });\n downloadSvg(svg, filename);\n },\n [],\n );\n\n return { exportPng, exportSvg, exportCsv, exportJson };\n}\n","/**\n * SVG export utility for generating publication-quality vector figures.\n *\n * Serializes a chart's SVG element to a standalone SVG string with\n * embedded spectral data paths.\n */\n\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { Spectrum } from \"../types\";\nimport { getSpectrumColor } from \"./colors\";\n\n/** Line dash patterns for different line styles. */\nexport const LINE_DASH_PATTERNS: Record<string, string> = {\n solid: \"\",\n dashed: \"8 4\",\n dotted: \"2 2\",\n \"dash-dot\": \"8 4 2 4\",\n};\n\n/** Supported line style values. */\nexport type LineStyle = \"solid\" | \"dashed\" | \"dotted\" | \"dash-dot\";\n\nexport interface SvgExportOptions {\n /** Width of the SVG. */\n width: number;\n /** Height of the SVG. */\n height: number;\n /** Background color. */\n background?: string;\n /** Title text for the SVG. */\n title?: string;\n}\n\n/**\n * Generate an SVG string for the given spectra.\n */\nexport function generateSvg(\n spectra: Spectrum[],\n xScale: ScaleLinear<number, number>,\n yScale: ScaleLinear<number, number>,\n options: SvgExportOptions,\n): string {\n const { width, height, background = \"#ffffff\", title } = options;\n\n const paths = spectra\n .filter((s) => s.visible !== false)\n .map((s, i) => {\n const color = s.color ?? getSpectrumColor(i);\n const lineStyle = (s as SpectrumWithStyle).lineStyle ?? \"solid\";\n const lineWidth = (s as SpectrumWithStyle).lineWidth ?? 1.5;\n const dashArray = LINE_DASH_PATTERNS[lineStyle] ?? \"\";\n\n const n = Math.min(s.x.length, s.y.length);\n if (n < 2) return \"\";\n\n const points: string[] = [];\n for (let j = 0; j < n; j++) {\n const px = xScale(s.x[j] as number).toFixed(2);\n const py = yScale(s.y[j] as number).toFixed(2);\n points.push(`${j === 0 ? \"M\" : \"L\"}${px},${py}`);\n }\n\n return `<path d=\"${points.join(\" \")}\" fill=\"none\" stroke=\"${color}\" stroke-width=\"${lineWidth}\"${dashArray ? ` stroke-dasharray=\"${dashArray}\"` : \"\"}/>\\n <!-- ${s.label} -->`;\n })\n .filter(Boolean)\n .join(\"\\n \");\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\" viewBox=\"0 0 ${width} ${height}\">\n <rect width=\"${width}\" height=\"${height}\" fill=\"${background}\"/>\n ${title ? `<text x=\"${width / 2}\" y=\"20\" text-anchor=\"middle\" font-family=\"system-ui\" font-size=\"14\">${title}</text>` : \"\"}\n <g>\n ${paths}\n </g>\n</svg>`;\n}\n\n/**\n * Download an SVG string as a file.\n */\nexport function downloadSvg(svg: string, filename = \"spectrum.svg\"): void {\n const blob = new Blob([svg], { type: \"image/svg+xml\" });\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n}\n\n/** Extended Spectrum with line style properties. */\ninterface SpectrumWithStyle extends Spectrum {\n lineStyle?: LineStyle;\n lineWidth?: number;\n}\n","/**\n * Export dropdown menu for saving spectra in various formats.\n */\n\nimport { useCallback, useState } from \"react\";\nimport type { Theme } from \"../../types\";\n\nexport interface ExportMenuProps {\n /** Theme for styling. */\n theme: Theme;\n /** Export as PNG. */\n onExportPng?: () => void;\n /** Export as SVG. */\n onExportSvg?: () => void;\n /** Export as CSV. */\n onExportCsv?: () => void;\n /** Export as JSON. */\n onExportJson?: () => void;\n}\n\nconst menuButtonStyle = (theme: Theme): React.CSSProperties => ({\n display: \"inline-flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n height: 28,\n padding: \"0 8px\",\n border: `1px solid ${theme === \"dark\" ? \"#4b5563\" : \"#d1d5db\"}`,\n borderRadius: 4,\n background: theme === \"dark\" ? \"#1f2937\" : \"#ffffff\",\n color: theme === \"dark\" ? \"#d1d5db\" : \"#374151\",\n fontSize: 12,\n cursor: \"pointer\",\n lineHeight: 1,\n position: \"relative\" as const,\n});\n\nconst dropdownStyle = (theme: Theme): React.CSSProperties => ({\n position: \"absolute\" as const,\n top: 30,\n left: 0,\n background: theme === \"dark\" ? \"#1f2937\" : \"#ffffff\",\n border: `1px solid ${theme === \"dark\" ? \"#4b5563\" : \"#d1d5db\"}`,\n borderRadius: 4,\n boxShadow: \"0 2px 8px rgba(0,0,0,0.15)\",\n zIndex: 200,\n minWidth: 100,\n overflow: \"hidden\",\n});\n\nconst optionStyle = (theme: Theme): React.CSSProperties => ({\n display: \"block\",\n width: \"100%\",\n padding: \"6px 12px\",\n border: \"none\",\n background: \"transparent\",\n color: theme === \"dark\" ? \"#d1d5db\" : \"#374151\",\n fontSize: 12,\n textAlign: \"left\" as const,\n cursor: \"pointer\",\n});\n\nexport function ExportMenu({\n theme,\n onExportPng,\n onExportSvg,\n onExportCsv,\n onExportJson,\n}: ExportMenuProps) {\n const [open, setOpen] = useState(false);\n\n const handleSelect = useCallback(\n (handler?: () => void) => {\n setOpen(false);\n handler?.();\n },\n [],\n );\n\n return (\n <div style={{ position: \"relative\", display: \"inline-block\" }}>\n <button\n type=\"button\"\n style={menuButtonStyle(theme)}\n onClick={() => setOpen(!open)}\n aria-label=\"Export\"\n aria-expanded={open}\n aria-haspopup=\"true\"\n >\n Export\n </button>\n {open && (\n <div style={dropdownStyle(theme)} role=\"menu\">\n {onExportPng && (\n <button\n type=\"button\"\n role=\"menuitem\"\n style={optionStyle(theme)}\n onClick={() => handleSelect(onExportPng)}\n >\n PNG Image\n </button>\n )}\n {onExportSvg && (\n <button\n type=\"button\"\n role=\"menuitem\"\n style={optionStyle(theme)}\n onClick={() => handleSelect(onExportSvg)}\n >\n SVG Vector\n </button>\n )}\n {onExportCsv && (\n <button\n type=\"button\"\n role=\"menuitem\"\n style={optionStyle(theme)}\n onClick={() => handleSelect(onExportCsv)}\n >\n CSV Data\n </button>\n )}\n {onExportJson && (\n <button\n type=\"button\"\n role=\"menuitem\"\n style={optionStyle(theme)}\n onClick={() => handleSelect(onExportJson)}\n >\n JSON Data\n </button>\n )}\n </div>\n )}\n </div>\n );\n}\n","/**\n * Spectral processing utilities.\n *\n * Pure functions for common spectral data transformations:\n * - Baseline correction (rubber-band)\n * - Normalization (min-max, area, SNV)\n * - Smoothing (Savitzky-Golay)\n * - Numerical derivatives (1st, 2nd)\n *\n * All functions return new arrays, never mutating inputs.\n *\n * @module processing\n */\n\n// ─── Baseline Correction ───────────────────────────────────────────\n\n/**\n * Rubber-band baseline correction.\n *\n * Computes the convex hull of the spectrum from below, then subtracts\n * the interpolated baseline. This is a simple, robust method for\n * removing broad fluorescence backgrounds.\n *\n * @param y - Input Y values\n * @returns Baseline-corrected Y values\n */\nexport function baselineRubberBand(y: Float64Array | number[]): Float64Array {\n const n = y.length;\n if (n < 3) return new Float64Array(y);\n\n // Find convex hull from below (lower envelope)\n const hullIndices: number[] = [0];\n for (let i = 1; i < n; i++) {\n while (hullIndices.length >= 2) {\n const j = hullIndices.length - 1;\n const a = hullIndices[j - 1];\n const b = hullIndices[j];\n // Cross product test: for lower hull, pop if b is above line a→i\n const cross =\n (i - a) * ((y[b] as number) - (y[a] as number)) -\n (b - a) * ((y[i] as number) - (y[a] as number));\n if (cross >= 0) {\n hullIndices.pop();\n } else {\n break;\n }\n }\n hullIndices.push(i);\n }\n\n // Interpolate baseline from hull\n const baseline = new Float64Array(n);\n let hi = 0;\n for (let i = 0; i < n; i++) {\n while (hi < hullIndices.length - 1 && hullIndices[hi + 1] <= i) {\n hi++;\n }\n if (hi >= hullIndices.length - 1) {\n baseline[i] = y[hullIndices[hullIndices.length - 1]] as number;\n } else {\n const a = hullIndices[hi];\n const b = hullIndices[hi + 1];\n const t = (i - a) / (b - a);\n baseline[i] =\n (y[a] as number) * (1 - t) + (y[b] as number) * t;\n }\n }\n\n // Subtract baseline\n const result = new Float64Array(n);\n for (let i = 0; i < n; i++) {\n result[i] = (y[i] as number) - baseline[i];\n }\n return result;\n}\n\n// ─── Normalization ─────────────────────────────────────────────────\n\n/**\n * Min-max normalization to [0, 1] range.\n */\nexport function normalizeMinMax(y: Float64Array | number[]): Float64Array {\n const n = y.length;\n const result = new Float64Array(n);\n let min = Infinity;\n let max = -Infinity;\n\n for (let i = 0; i < n; i++) {\n const v = y[i] as number;\n if (v < min) min = v;\n if (v > max) max = v;\n }\n\n const range = max - min;\n if (range === 0) return result; // all zeros\n\n for (let i = 0; i < n; i++) {\n result[i] = ((y[i] as number) - min) / range;\n }\n return result;\n}\n\n/**\n * Area normalization: divide by total area under the curve.\n *\n * Uses trapezoidal integration. The resulting spectrum has unit area.\n */\nexport function normalizeArea(\n x: Float64Array | number[],\n y: Float64Array | number[],\n): Float64Array {\n const n = Math.min(x.length, y.length);\n if (n < 2) return new Float64Array(y);\n\n // Trapezoidal area\n let area = 0;\n for (let i = 1; i < n; i++) {\n area +=\n Math.abs((x[i] as number) - (x[i - 1] as number)) *\n (Math.abs(y[i] as number) + Math.abs(y[i - 1] as number)) *\n 0.5;\n }\n\n if (area === 0) return new Float64Array(y);\n\n const result = new Float64Array(n);\n for (let i = 0; i < n; i++) {\n result[i] = (y[i] as number) / area;\n }\n return result;\n}\n\n/**\n * Standard Normal Variate (SNV) normalization.\n *\n * Centers the spectrum by subtracting the mean, then divides by\n * the standard deviation. Common in chemometrics.\n */\nexport function normalizeSNV(y: Float64Array | number[]): Float64Array {\n const n = y.length;\n if (n === 0) return new Float64Array(0);\n\n let sum = 0;\n for (let i = 0; i < n; i++) {\n sum += y[i] as number;\n }\n const mean = sum / n;\n\n let variance = 0;\n for (let i = 0; i < n; i++) {\n const d = (y[i] as number) - mean;\n variance += d * d;\n }\n const std = Math.sqrt(variance / n);\n\n if (std === 0) return new Float64Array(n);\n\n const result = new Float64Array(n);\n for (let i = 0; i < n; i++) {\n result[i] = ((y[i] as number) - mean) / std;\n }\n return result;\n}\n\n// ─── Smoothing ─────────────────────────────────────────────────────\n\n/**\n * Savitzky-Golay smoothing (polynomial order 2).\n *\n * Uses pre-computed convolution coefficients for common window sizes.\n * Falls back to a simple moving average for unsupported window sizes.\n *\n * @param y - Input Y values\n * @param windowSize - Must be odd and >= 3. Defaults to 5.\n * @returns Smoothed Y values\n */\nexport function smoothSavitzkyGolay(\n y: Float64Array | number[],\n windowSize = 5,\n): Float64Array {\n const n = y.length;\n if (n < windowSize || windowSize < 3) return new Float64Array(y);\n\n // Ensure odd window\n const w = windowSize % 2 === 0 ? windowSize + 1 : windowSize;\n const halfW = (w - 1) / 2;\n\n // Pre-computed SG coefficients (quadratic, normalized)\n const coefficients = getSGCoefficients(w);\n\n const result = new Float64Array(n);\n\n // Copy edges unchanged\n for (let i = 0; i < halfW; i++) {\n result[i] = y[i] as number;\n result[n - 1 - i] = y[n - 1 - i] as number;\n }\n\n // Apply convolution\n for (let i = halfW; i < n - halfW; i++) {\n let sum = 0;\n for (let j = -halfW; j <= halfW; j++) {\n sum += coefficients[j + halfW] * (y[i + j] as number);\n }\n result[i] = sum;\n }\n\n return result;\n}\n\n/**\n * Get Savitzky-Golay coefficients for quadratic polynomial smoothing.\n */\nfunction getSGCoefficients(windowSize: number): number[] {\n // Pre-computed for common sizes (quadratic smoothing)\n const precomputed: Record<number, number[]> = {\n 5: [-3, 12, 17, 12, -3].map((v) => v / 35),\n 7: [-2, 3, 6, 7, 6, 3, -2].map((v) => v / 21),\n 9: [-21, 14, 39, 54, 59, 54, 39, 14, -21].map((v) => v / 231),\n 11: [-36, 9, 44, 69, 84, 89, 84, 69, 44, 9, -36].map((v) => v / 429),\n };\n\n if (precomputed[windowSize]) {\n return precomputed[windowSize];\n }\n\n // Fallback: uniform weights (simple moving average)\n return Array(windowSize).fill(1 / windowSize);\n}\n\n// ─── Derivatives ───────────────────────────────────────────────────\n\n/**\n * First derivative using central differences.\n *\n * @param x - X values (for spacing)\n * @param y - Y values\n * @returns First derivative dy/dx\n */\nexport function derivative1st(\n x: Float64Array | number[],\n y: Float64Array | number[],\n): Float64Array {\n const n = Math.min(x.length, y.length);\n if (n < 2) return new Float64Array(n);\n\n const result = new Float64Array(n);\n\n // Forward difference for first point\n result[0] =\n ((y[1] as number) - (y[0] as number)) /\n ((x[1] as number) - (x[0] as number));\n\n // Central differences for interior\n for (let i = 1; i < n - 1; i++) {\n result[i] =\n ((y[i + 1] as number) - (y[i - 1] as number)) /\n ((x[i + 1] as number) - (x[i - 1] as number));\n }\n\n // Backward difference for last point\n result[n - 1] =\n ((y[n - 1] as number) - (y[n - 2] as number)) /\n ((x[n - 1] as number) - (x[n - 2] as number));\n\n return result;\n}\n\n/**\n * Second derivative using central differences.\n *\n * @param x - X values (for spacing)\n * @param y - Y values\n * @returns Second derivative d²y/dx²\n */\nexport function derivative2nd(\n x: Float64Array | number[],\n y: Float64Array | number[],\n): Float64Array {\n const n = Math.min(x.length, y.length);\n if (n < 3) return new Float64Array(n);\n\n const result = new Float64Array(n);\n\n // Interior points using central difference\n for (let i = 1; i < n - 1; i++) {\n const dx1 = (x[i] as number) - (x[i - 1] as number);\n const dx2 = (x[i + 1] as number) - (x[i] as number);\n const dxAvg = (dx1 + dx2) / 2;\n result[i] =\n ((y[i + 1] as number) - 2 * (y[i] as number) + (y[i - 1] as number)) /\n (dxAvg * dxAvg);\n }\n\n // Copy boundary values from nearest interior\n result[0] = result[1];\n result[n - 1] = result[n - 2];\n\n return result;\n}\n","/**\n * Spectrum comparison and mathematical operations.\n *\n * Provides functions for computing difference spectra, correlation\n * coefficients, spectral arithmetic, and residuals.\n *\n * All functions assume spectra share the same X-axis (same length\n * and point spacing). Use `interpolateToGrid` to align spectra\n * before comparison if needed.\n *\n * @module comparison\n */\n\nimport type { Spectrum } from \"../types\";\n\n/** Auto-incrementing ID counter for generated spectra. */\nlet idCounter = 0;\n\n/**\n * Compute the difference spectrum (a - b).\n *\n * @param a - First spectrum\n * @param b - Second spectrum\n * @returns New Spectrum representing a - b\n */\nexport function differenceSpectrum(a: Spectrum, b: Spectrum): Spectrum {\n const n = Math.min(a.y.length, b.y.length);\n const y = new Float64Array(n);\n for (let i = 0; i < n; i++) {\n y[i] = (a.y[i] as number) - (b.y[i] as number);\n }\n\n return {\n id: `diff-${++idCounter}`,\n label: `${a.label} − ${b.label}`,\n x: a.x.length <= b.x.length ? new Float64Array(a.x) : new Float64Array(b.x),\n y,\n xUnit: a.xUnit,\n yUnit: a.yUnit,\n color: \"#ef4444\",\n type: a.type,\n };\n}\n\n/**\n * Add two spectra element-wise.\n */\nexport function addSpectra(a: Spectrum, b: Spectrum): Spectrum {\n const n = Math.min(a.y.length, b.y.length);\n const y = new Float64Array(n);\n for (let i = 0; i < n; i++) {\n y[i] = (a.y[i] as number) + (b.y[i] as number);\n }\n\n return {\n id: `add-${++idCounter}`,\n label: `${a.label} + ${b.label}`,\n x: a.x.length <= b.x.length ? new Float64Array(a.x) : new Float64Array(b.x),\n y,\n xUnit: a.xUnit,\n yUnit: a.yUnit,\n type: a.type,\n };\n}\n\n/**\n * Multiply spectrum Y values by a scalar factor.\n */\nexport function scaleSpectrum(spectrum: Spectrum, factor: number): Spectrum {\n const n = spectrum.y.length;\n const y = new Float64Array(n);\n for (let i = 0; i < n; i++) {\n y[i] = (spectrum.y[i] as number) * factor;\n }\n\n return {\n id: `scaled-${++idCounter}`,\n label: `${spectrum.label} × ${factor}`,\n x: new Float64Array(spectrum.x),\n y,\n xUnit: spectrum.xUnit,\n yUnit: spectrum.yUnit,\n color: spectrum.color,\n type: spectrum.type,\n };\n}\n\n/**\n * Pearson correlation coefficient between two spectra's Y values.\n *\n * Returns a value in [-1, 1] where 1 means perfect positive\n * correlation and 0 means no linear correlation.\n */\nexport function correlationCoefficient(a: Spectrum, b: Spectrum): number {\n const n = Math.min(a.y.length, b.y.length);\n if (n === 0) return 0;\n\n let sumA = 0;\n let sumB = 0;\n for (let i = 0; i < n; i++) {\n sumA += a.y[i] as number;\n sumB += b.y[i] as number;\n }\n const meanA = sumA / n;\n const meanB = sumB / n;\n\n let cov = 0;\n let varA = 0;\n let varB = 0;\n for (let i = 0; i < n; i++) {\n const da = (a.y[i] as number) - meanA;\n const db = (b.y[i] as number) - meanB;\n cov += da * db;\n varA += da * da;\n varB += db * db;\n }\n\n const denom = Math.sqrt(varA * varB);\n if (denom === 0) return 0;\n return cov / denom;\n}\n\n/**\n * Compute the residual (absolute difference) between two spectra.\n */\nexport function residualSpectrum(a: Spectrum, b: Spectrum): Spectrum {\n const n = Math.min(a.y.length, b.y.length);\n const y = new Float64Array(n);\n for (let i = 0; i < n; i++) {\n y[i] = Math.abs((a.y[i] as number) - (b.y[i] as number));\n }\n\n return {\n id: `residual-${++idCounter}`,\n label: `|${a.label} − ${b.label}|`,\n x: a.x.length <= b.x.length ? new Float64Array(a.x) : new Float64Array(b.x),\n y,\n xUnit: a.xUnit,\n yUnit: a.yUnit,\n color: \"#f97316\",\n lineStyle: \"dashed\",\n type: a.type,\n };\n}\n\n/**\n * Interpolate a spectrum to a new X grid using linear interpolation.\n *\n * Useful for aligning two spectra that have different X-axis points\n * before performing comparison operations.\n *\n * @param spectrum - Source spectrum\n * @param newX - Target X values\n * @returns Spectrum interpolated to the new X grid\n */\nexport function interpolateToGrid(\n spectrum: Spectrum,\n newX: Float64Array | number[],\n): Spectrum {\n const n = Math.min(spectrum.x.length, spectrum.y.length);\n const m = newX.length;\n const y = new Float64Array(m);\n\n if (n < 2) return { ...spectrum, x: new Float64Array(newX), y };\n\n // Determine direction of source x (ascending or descending)\n const ascending = (spectrum.x[n - 1] as number) > (spectrum.x[0] as number);\n\n for (let j = 0; j < m; j++) {\n const targetX = newX[j] as number;\n\n // Find bracketing indices using binary search\n let lo = 0;\n let hi = n - 1;\n\n while (lo < hi - 1) {\n const mid = (lo + hi) >>> 1;\n if (ascending) {\n if ((spectrum.x[mid] as number) <= targetX) lo = mid;\n else hi = mid;\n } else {\n if ((spectrum.x[mid] as number) >= targetX) lo = mid;\n else hi = mid;\n }\n }\n\n // Linear interpolation\n const x0 = spectrum.x[lo] as number;\n const x1 = spectrum.x[hi] as number;\n const y0 = spectrum.y[lo] as number;\n const y1 = spectrum.y[hi] as number;\n\n if (x0 === x1) {\n y[j] = y0;\n } else {\n const t = (targetX - x0) / (x1 - x0);\n y[j] = y0 + t * (y1 - y0);\n }\n }\n\n return {\n ...spectrum,\n id: `interp-${++idCounter}`,\n x: new Float64Array(newX),\n y,\n };\n}\n","/**\n * SPC file parser for Thermo/Galactic spectral data format.\n *\n * Parses the binary SPC format used by GRAMS, Thermo Scientific,\n * PerkinElmer, and other spectroscopy software.\n *\n * Supports:\n * - Single and multi-spectrum files\n * - Even and uneven X spacing\n * - 32-bit float and 16-bit integer Y data\n * - File header metadata (resolution, instrument, etc.)\n *\n * Reference: \"The New Galactic SPC File Format\" specification\n *\n * @module spc\n */\n\nimport type { Spectrum, SpectrumType } from \"../types\";\n\n/** Auto-incrementing ID counter. */\nlet idCounter = 0;\n\n/** SPC file type flags. */\nconst TSPREC = 0x01; // Y data is 16-bit integer\nconst TMULTI = 0x04; // Multi-file (multiple spectra)\nconst TXVALS = 0x80; // Non-evenly spaced X data present\n\n/** SPC X-axis type codes to units. */\nconst X_TYPE_LABELS: Record<number, string> = {\n 0: \"Arbitrary\",\n 1: \"cm⁻¹\",\n 2: \"µm\",\n 3: \"nm\",\n 4: \"s\",\n 5: \"min\",\n 6: \"Hz\",\n 7: \"kHz\",\n 8: \"MHz\",\n 9: \"m/z\",\n 10: \"Da\",\n 11: \"ppm\",\n 12: \"days\",\n 13: \"years\",\n 14: \"Raman shift (cm⁻¹)\",\n 15: \"eV\",\n 16: \"Text label\",\n 255: \"Double interferogram\",\n};\n\n/** SPC Y-axis type codes to units. */\nconst Y_TYPE_LABELS: Record<number, string> = {\n 0: \"Arbitrary\",\n 1: \"Interferogram\",\n 2: \"Absorbance\",\n 3: \"Kubelka-Munk\",\n 4: \"Counts\",\n 5: \"V\",\n 6: \"°\",\n 7: \"mA\",\n 8: \"mm\",\n 9: \"mV\",\n 10: \"log(1/R)\",\n 11: \"%\",\n 12: \"Intensity\",\n 13: \"Relative intensity\",\n 14: \"Energy\",\n 16: \"dB\",\n 19: \"°C\",\n 20: \"°F\",\n 21: \"K\",\n 22: \"Index of refraction [n]\",\n 23: \"Extinction coeff. [k]\",\n 24: \"Real\",\n 25: \"Imaginary\",\n 26: \"Complex\",\n 128: \"Transmittance\",\n 129: \"Reflectance\",\n 130: \"Arbitrary (Valley to peak)\",\n 131: \"Emission\",\n};\n\n/** Infer SpectrumType from SPC X-type code. */\nfunction inferSpectrumType(xType: number, yType: number): SpectrumType {\n if (xType === 1) return \"IR\"; // cm⁻¹\n if (xType === 14) return \"Raman\";\n if (xType === 3 && (yType === 2 || yType === 128)) return \"UV-Vis\";\n if (xType === 2) return \"NIR\"; // µm\n if (yType === 131) return \"fluorescence\";\n return \"other\";\n}\n\n/**\n * Parse an SPC binary file into Spectrum objects.\n *\n * @param buffer - ArrayBuffer containing the SPC file data\n * @returns Array of parsed Spectrum objects\n * @throws Error if the file is not a valid SPC file\n */\nexport function parseSpc(buffer: ArrayBuffer): Spectrum[] {\n const view = new DataView(buffer);\n const minSize = 512; // SPC header is 512 bytes\n\n if (buffer.byteLength < minSize) {\n throw new Error(\"Invalid SPC file: too small for SPC header\");\n }\n\n // Read main file header (512 bytes)\n const flags = view.getUint8(0);\n const fileVersion = view.getUint8(1);\n\n // Validate version (0x4B = new format, 0x4D = old format)\n if (fileVersion !== 0x4b && fileVersion !== 0x4d) {\n throw new Error(\n `Unsupported SPC version: 0x${fileVersion.toString(16)}. Expected 0x4B or 0x4D.`,\n );\n }\n\n const xType = view.getUint8(2);\n const yType = view.getUint8(3);\n const npoints = view.getUint32(4, true); // little-endian\n const firstX = view.getFloat64(8, true);\n const lastX = view.getFloat64(16, true);\n const numSpectra = view.getUint32(24, true);\n\n // Experiment type at offset 28\n const xUnit = X_TYPE_LABELS[xType] ?? \"Arbitrary\";\n const yUnit = Y_TYPE_LABELS[yType] ?? \"Arbitrary\";\n\n // Read memo string (offset 30, 130 bytes max)\n const memoBytes = new Uint8Array(buffer, 30, 130);\n const memo = decodeText(memoBytes);\n\n const isMulti = (flags & TMULTI) !== 0;\n const hasXValues = (flags & TXVALS) !== 0;\n const is16Bit = (flags & TSPREC) !== 0;\n const specType = inferSpectrumType(xType, yType);\n\n // Generate evenly-spaced X values if not provided\n let sharedX: Float64Array | null = null;\n\n if (!hasXValues && npoints > 0) {\n sharedX = new Float64Array(npoints);\n const step = npoints > 1 ? (lastX - firstX) / (npoints - 1) : 0;\n for (let i = 0; i < npoints; i++) {\n sharedX[i] = firstX + i * step;\n }\n }\n\n const spectra: Spectrum[] = [];\n let offset = 512; // Start after main header\n\n // Read X values if present (only for non-multi or old format)\n let fileXValues: Float64Array | null = null;\n if (hasXValues && !isMulti) {\n fileXValues = new Float64Array(npoints);\n for (let i = 0; i < npoints; i++) {\n fileXValues[i] = view.getFloat32(offset, true);\n offset += 4;\n }\n }\n\n const count = isMulti ? numSpectra : 1;\n\n for (let s = 0; s < count; s++) {\n let xVals: Float64Array;\n let yVals: Float64Array;\n let subNpoints = npoints;\n\n if (isMulti) {\n // Read sub-file header (32 bytes)\n if (offset + 32 > buffer.byteLength) break;\n\n // Sub-header fields: flags(1), exp(1), index(2), startX(4), endX(4), npoints(4), ...\n const subStartX = view.getFloat32(offset + 4, true);\n const subEndX = view.getFloat32(offset + 8, true);\n subNpoints = view.getUint32(offset + 12, true) || npoints;\n offset += 32;\n\n // Sub-file X values\n if (hasXValues) {\n xVals = new Float64Array(subNpoints);\n for (let i = 0; i < subNpoints; i++) {\n if (offset + 4 > buffer.byteLength) break;\n xVals[i] = view.getFloat32(offset, true);\n offset += 4;\n }\n } else if (sharedX) {\n xVals = sharedX;\n } else {\n // Generate from sub-header\n xVals = new Float64Array(subNpoints);\n const step = subNpoints > 1 ? (subEndX - subStartX) / (subNpoints - 1) : 0;\n for (let i = 0; i < subNpoints; i++) {\n xVals[i] = subStartX + i * step;\n }\n }\n } else {\n xVals = fileXValues ?? sharedX ?? new Float64Array(0);\n }\n\n // Read Y values\n yVals = new Float64Array(subNpoints);\n\n if (is16Bit) {\n for (let i = 0; i < subNpoints; i++) {\n if (offset + 2 > buffer.byteLength) break;\n yVals[i] = view.getInt16(offset, true);\n offset += 2;\n }\n } else {\n for (let i = 0; i < subNpoints; i++) {\n if (offset + 4 > buffer.byteLength) break;\n yVals[i] = view.getFloat32(offset, true);\n offset += 4;\n }\n }\n\n spectra.push({\n id: `spc-${++idCounter}`,\n label: memo || `SPC Spectrum ${s + 1}`,\n x: xVals,\n y: yVals,\n xUnit,\n yUnit,\n type: specType,\n meta: {\n format: \"SPC\",\n version: fileVersion === 0x4b ? \"new\" : \"old\",\n xType: xType.toString(),\n yType: yType.toString(),\n },\n });\n }\n\n if (spectra.length === 0) {\n throw new Error(\"Invalid SPC file: no spectra found\");\n }\n\n return spectra;\n}\n\n/** Decode a null-terminated byte array to string. */\nfunction decodeText(bytes: Uint8Array): string {\n const nullIdx = bytes.indexOf(0);\n const slice = nullIdx >= 0 ? bytes.slice(0, nullIdx) : bytes;\n return new TextDecoder(\"ascii\").decode(slice).trim();\n}\n"],"mappings":"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","names":["index_exports","__export","AnnotationLayer","AxisLayer","Crosshair","DARK_THEME","DropZone","ExportMenu","KEYBOARD_SHORTCUTS","LIGHT_THEME","LINE_DASH_PATTERNS","Legend","Minimap","PeakMarkers","RegionSelector","SPECTRUM_COLORS","SpectraView","SpectrumCanvas","StackedView","Toolbar","Tooltip","addSpectra","baselineRubberBand","binarySearchClosest","computeXExtent","computeYExtent","correlationCoefficient","createXScale","createYScale","derivative1st","derivative2nd","detectPeaks","differenceSpectrum","downloadSvg","generateChartDescription","generateSvg","getSpectrumColor","getThemeColors","interpolateToGrid","lttbDownsample","normalizeArea","normalizeMinMax","normalizeSNV","parseCsv","parseCsvMulti","parseJcamp","parseJson","parseSpc","prefersReducedMotion","residualSpectrum","scaleSpectrum","smoothSavitzkyGolay","snapToNearestSpectrum","useExport","useKeyboardNavigation","usePeakPicking","useRegionSelect","useResizeObserver","useSpectrumData","useZoomPan","__toCommonJS","import_react","import_d3_scale","import_d3_array","Y_PADDING","computeXExtent","spectra","globalMin","globalMax","s","min","max","computeYExtent","pad","createXScale","domain","width","margin","reverseX","plotWidth","d","createYScale","height","plotHeight","SPECTRUM_COLORS","LIGHT_THEME","DARK_THEME","getSpectrumColor","index","getThemeColors","theme","import_react","import_d3_zoom","import_d3_selection","import_d3_transition","ZOOM_STEP","useZoomPan","options","plotWidth","plotHeight","xScale","yScale","scaleExtent","enabled","onViewChange","zoomRef","zoomBehaviorRef","onViewChangeRef","scaleExtentRef","transform","setTransform","zoomedXScale","zoomedYScale","element","zoomBehavior","event","newTransform","newXScale","newYScale","resetZoom","zoomIn","zoomOut","import_react","lttbDownsample","x","y","startIdx","endIdx","xScale","yScale","targetCount","n","result","i","bucketCount","bucketSize","prevSelectedIdx","bucket","bucketStart","bucketEnd","nextBucketStart","nextBucketEnd","avgX","avgY","avgCount","prevPx","prevPy","maxArea","bestIdx","px","py","area","LINE_WIDTH","CANVAS_DASH_PATTERNS","DECIMATION_THRESHOLD","clearCanvas","ctx","width","height","drawSpectrum","spectrum","index","xScale","yScale","plotWidth","options","highlighted","opacity","n","color","getSpectrumColor","baseWidth","lineWidth","dashPattern","xMin","xMax","domainMin","domainMax","startIdx","endIdx","i","visibleCount","targetPoints","points","lttbDownsample","started","px","py","drawAllSpectra","spectra","highlightedId","import_jsx_runtime","SpectrumCanvas","spectra","xScale","yScale","width","height","highlightedId","ref","canvasRef","dprRef","canvas","dpr","ctx","drawAllSpectra","import_jsx_runtime","generateTicks","scale","count","d0","d1","min","step","_","i","formatTick","value","AxisLayer","xScale","yScale","width","height","xLabel","yLabel","showGrid","colors","xTicks","yTicks","tick","import_jsx_runtime","PeakMarkers","peaks","xScale","yScale","colors","onPeakClick","xMin","xMax","domainMin","domainMax","visiblePeaks","p","peak","i","px","py","import_jsx_runtime","RegionSelector","regions","xScale","height","colors","region","i","x1","x2","left","w","import_jsx_runtime","Crosshair","position","width","height","colors","snapPoint","formatValue","v","import_jsx_runtime","AnnotationLayer","annotations","xScale","yScale","colors","ann","px","py","dx","dy","textX","textY","fontSize","color","showLine","binarySearchClosest","arr","target","length","ascending","lo","hi","mid","midVal","dLo","dHi","snapToNearestSpectrum","spectra","dataX","cursorPy","xScale","yScale","best","spectrum","n","idx","sx","sy","pxDist","pyDist","distance","import_react","import_jsx_runtime","buttonStyle","theme","toolbarStyle","Toolbar","onZoomIn","onZoomOut","onReset","isZoomed","import_react","import_jsx_runtime","containerStyle","theme","position","itemStyle","isHidden","isHighlighted","swatchStyle","color","Legend","spectra","onToggleVisibility","onHighlight","highlightedId","s","i","getSpectrumColor","import_react","import_jsx_runtime","DropZone","enabled","theme","width","height","onDrop","children","isDragging","setIsDragging","dragCountRef","handleDragEnter","e","handleDragLeave","handleDragOver","handleDrop","files","import_react","import_jsx_runtime","PANEL_GAP","StackedView","spectra","xScale","plotWidth","plotHeight","margin","theme","showGrid","xLabel","yLabel","visible","s","colors","getThemeColors","panelCount","totalGap","panelHeight","spectrum","i","yOffset","yExtent","computeYExtent","yScale","createYScale","color","getSpectrumColor","coloredSpectrum","AxisLayer","SpectrumCanvas","import_react","useRegionSelect","options","enabled","xScale","onRegionSelect","pendingRegion","setPendingRegion","dragStartRef","handleMouseDown","event","rect","px","dataX","handleMouseMove","start","handleMouseUp","import_react","useResizeObserver","size","setSize","observerRef","elementRef","ref","node","observer","entries","entry","width","height","import_react","useKeyboardNavigation","options","onZoomIn","onZoomOut","onReset","enabled","event","prefersReducedMotion","generateChartDescription","spectrumCount","xLabel","yLabel","KEYBOARD_SHORTCUTS","import_jsx_runtime","DEFAULT_MARGIN","DEFAULT_WIDTH","DEFAULT_HEIGHT","resolveConfig","props","inferLabels","spectra","xLabel","yLabel","first","SpectraView","peaks","regions","annotations","onPeakClick","onViewChange","onCrosshairMove","onToggleVisibility","onFileDrop","onRegionSelect","canvasRef","snapCrosshair","resizeRef","measuredSize","useResizeObserver","clipId","config","width","height","margin","reverseX","theme","plotWidth","plotHeight","colors","getThemeColors","labels","xExtent","computeXExtent","yExtent","computeYExtent","baseXScale","createXScale","baseYScale","createYScale","onViewChangeRef","stableOnViewChange","xDomain","yDomain","zoomRef","zoomState","zoomedXScale","zoomedYScale","resetZoom","zoomIn","zoomOut","useZoomPan","pendingRegion","regionMouseDown","regionMouseMove","regionMouseUp","useRegionSelect","highlightedId","setHighlightedId","crosshairPos","setCrosshairPos","snapPointState","setSnapPointState","onCrosshairMoveRef","handleMouseMove","event","rect","px","py","dataX","dataY","snap","snapToNearestSpectrum","spIdx","s","getSpectrumColor","handleMouseLeave","handleKeyDown","useKeyboardNavigation","chartDescription","generateChartDescription","isStacked","toolbarHeight","Toolbar","Legend","DropZone","StackedView","SpectrumCanvas","AxisLayer","RegionSelector","PeakMarkers","AnnotationLayer","Crosshair","e","import_react","import_d3_scale","import_jsx_runtime","Minimap","spectra","xExtent","yExtent","visibleXDomain","width","height","theme","isZoomed","canvasRef","colors","getThemeColors","xScale","yScale","ctx","s","spectrum","n","color","getSpectrumColor","step","started","i","px","py","vpLeft","vpRight","vpWidth","import_react","import_jsx_runtime","formatNumber","v","format","Tooltip","data","spectra","peaks","plotWidth","plotHeight","colors","numberFormat","entries","s","spectrum","i","n","idx","binarySearchClosest","getSpectrumColor","nearestPeak","best","bestDist","peak","dist","lineHeight","headerHeight","peakLineHeight","tooltipHeight","tooltipWidth","tx","ty","entry","import_react","detectPeaks","x","y","options","prominence","minDistance","maxPeaks","yMin","yMax","i","signalRange","absProminence","candidates","leftMin","findMinBefore","rightMin","findMinAfter","prom","a","b","kept","c","k","formatWavenumber","min","j","value","usePeakPicking","spectra","options","enabled","spectrumIds","prominence","minDistance","maxPeaks","targetSpectra","s","allPeaks","spectrum","peaks","detectPeaks","peak","import_react","idCounter","DELIMITER_CANDIDATES","detectDelimiter","text","lines","bestDelimiter","bestScore","d","counts","line","minCount","c","parseCsv","options","xColumn","yColumn","hasHeader","label","delimiter","headerLabel","startIndex","headers","h","xValues","yValues","i","parts","xVal","yVal","parseCsvMulti","numColumns","yArrays","col","xArray","yArr","idCounter","parseJson","text","data","item","i","parseSingleJson","obj","input","index","xRaw","yRaw","label","idCounter","converterModule","converterChecked","getConverter","inferType","info","dataType","parseJcamp","text","converter","parseWithConverter","parseBasicJcamp","entry","i","firstSpectrum","lines","xValues","yValues","inData","line","trimmed","match","key","value","values","x0","firstX","lastX","npoints","deltaX","j","detectFormat","filename","useSpectrumData","initialSpectra","spectra","setSpectra","loading","setLoading","error","setError","loadText","text","format","parsed","parseJcamp","parseCsv","parseJson","prev","err","message","loadFile","file","addSpectrum","spectrum","removeSpectrum","id","s","toggleVisibility","clear","import_react","LINE_DASH_PATTERNS","generateSvg","spectra","xScale","yScale","options","width","height","background","title","paths","s","i","color","getSpectrumColor","lineStyle","lineWidth","dashArray","n","points","j","px","py","downloadSvg","svg","filename","blob","url","a","downloadBlob","blob","filename","url","a","useExport","exportPng","canvas","exportCsv","spectra","visible","s","header","rows","x","i","csv","maxLen","values","exportJson","output","json","exportSvg","xScale","yScale","width","height","svg","generateSvg","downloadSvg","import_react","import_jsx_runtime","menuButtonStyle","theme","dropdownStyle","optionStyle","ExportMenu","onExportPng","onExportSvg","onExportCsv","onExportJson","open","setOpen","handleSelect","handler","baselineRubberBand","y","n","hullIndices","i","j","a","b","baseline","hi","t","result","normalizeMinMax","min","max","v","range","normalizeArea","x","area","normalizeSNV","sum","mean","variance","d","std","smoothSavitzkyGolay","windowSize","w","halfW","coefficients","getSGCoefficients","precomputed","derivative1st","derivative2nd","dx1","dx2","dxAvg","idCounter","differenceSpectrum","a","b","n","y","i","addSpectra","scaleSpectrum","spectrum","factor","correlationCoefficient","sumA","sumB","meanA","meanB","cov","varA","varB","da","db","denom","residualSpectrum","interpolateToGrid","newX","m","ascending","j","targetX","lo","hi","mid","x0","x1","y0","y1","t","idCounter","TSPREC","TMULTI","TXVALS","X_TYPE_LABELS","Y_TYPE_LABELS","inferSpectrumType","xType","yType","parseSpc","buffer","view","flags","fileVersion","npoints","firstX","lastX","numSpectra","xUnit","yUnit","memoBytes","memo","decodeText","isMulti","hasXValues","is16Bit","specType","sharedX","step","i","spectra","offset","fileXValues","count","s","xVals","yVals","subNpoints","subStartX","subEndX","bytes","nullIdx","slice"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/components/SpectraView/SpectraView.tsx","../src/utils/scales.ts","../src/utils/colors.ts","../src/hooks/useZoomPan.ts","../src/components/SpectrumCanvas/SpectrumCanvas.tsx","../src/utils/lttb.ts","../src/utils/rendering.ts","../src/components/AxisLayer/AxisLayer.tsx","../src/components/PeakMarkers/PeakMarkers.tsx","../src/components/RegionSelector/RegionSelector.tsx","../src/components/Crosshair/Crosshair.tsx","../src/components/AnnotationLayer/AnnotationLayer.tsx","../src/utils/snap.ts","../src/components/Toolbar/Toolbar.tsx","../src/components/Legend/Legend.tsx","../src/components/DropZone/DropZone.tsx","../src/components/StackedView/StackedView.tsx","../src/hooks/useRegionSelect.ts","../src/hooks/useResizeObserver.ts","../src/hooks/useKeyboardNavigation.ts","../src/utils/a11y.ts","../src/components/Minimap/Minimap.tsx","../src/components/Tooltip/Tooltip.tsx","../src/hooks/usePeakPicking.ts","../src/utils/peaks.ts","../src/hooks/useSpectrumData.ts","../src/parsers/csv.ts","../src/parsers/json.ts","../src/parsers/jcamp.ts","../src/hooks/useExport.ts","../src/utils/svg-export.ts","../src/hooks/useNormalization.ts","../src/utils/processing.ts","../src/hooks/useHistory.ts","../src/components/ExportMenu/ExportMenu.tsx","../src/utils/comparison.ts","../src/parsers/spc.ts"],"sourcesContent":["/**\n * SpectraView — Interactive React component for vibrational spectroscopy.\n *\n * @example\n * ```tsx\n * import { SpectraView, parseCsv } from \"spectraview\";\n *\n * const spectrum = parseCsv(csvText);\n * <SpectraView spectra={[spectrum]} reverseX />\n * ```\n *\n * @packageDocumentation\n */\n\n// Main component\nexport { SpectraView } from \"./components/SpectraView/SpectraView\";\n\n// Sub-components (for advanced composition)\nexport { SpectrumCanvas } from \"./components/SpectrumCanvas/SpectrumCanvas\";\nexport { AxisLayer } from \"./components/AxisLayer/AxisLayer\";\nexport { PeakMarkers } from \"./components/PeakMarkers/PeakMarkers\";\nexport { RegionSelector } from \"./components/RegionSelector/RegionSelector\";\nexport { Crosshair } from \"./components/Crosshair/Crosshair\";\nexport { Toolbar } from \"./components/Toolbar/Toolbar\";\nexport { Legend } from \"./components/Legend/Legend\";\nexport { DropZone } from \"./components/DropZone/DropZone\";\nexport { AnnotationLayer } from \"./components/AnnotationLayer/AnnotationLayer\";\nexport { Minimap } from \"./components/Minimap/Minimap\";\nexport { Tooltip } from \"./components/Tooltip/Tooltip\";\n\n// Hooks\nexport { useZoomPan } from \"./hooks/useZoomPan\";\nexport { usePeakPicking } from \"./hooks/usePeakPicking\";\nexport { useSpectrumData } from \"./hooks/useSpectrumData\";\nexport { useExport } from \"./hooks/useExport\";\nexport { useRegionSelect } from \"./hooks/useRegionSelect\";\nexport { useResizeObserver } from \"./hooks/useResizeObserver\";\nexport { useKeyboardNavigation } from \"./hooks/useKeyboardNavigation\";\nexport { useNormalization } from \"./hooks/useNormalization\";\nexport { useHistory } from \"./hooks/useHistory\";\nexport { StackedView } from \"./components/StackedView/StackedView\";\nexport { ExportMenu } from \"./components/ExportMenu/ExportMenu\";\nexport { generateSvg, downloadSvg, LINE_DASH_PATTERNS } from \"./utils/svg-export\";\nexport {\n prefersReducedMotion,\n generateChartDescription,\n KEYBOARD_SHORTCUTS,\n} from \"./utils/a11y\";\nexport { binarySearchClosest, snapToNearestSpectrum } from \"./utils/snap\";\nexport { lttbDownsample } from \"./utils/lttb\";\nexport {\n baselineRubberBand,\n normalizeMinMax,\n normalizeArea,\n normalizeSNV,\n smoothSavitzkyGolay,\n derivative1st,\n derivative2nd,\n} from \"./utils/processing\";\nexport {\n differenceSpectrum,\n addSpectra,\n scaleSpectrum,\n correlationCoefficient,\n residualSpectrum,\n interpolateToGrid,\n} from \"./utils/comparison\";\n\n// Parsers\nexport { parseJcamp } from \"./parsers/jcamp\";\nexport { parseCsv, parseCsvMulti } from \"./parsers/csv\";\nexport { parseJson } from \"./parsers/json\";\nexport { parseSpc } from \"./parsers/spc\";\n\n// Utilities\nexport { detectPeaks } from \"./utils/peaks\";\nexport {\n computeXExtent,\n computeYExtent,\n createXScale,\n createYScale,\n} from \"./utils/scales\";\nexport {\n SPECTRUM_COLORS,\n LIGHT_THEME,\n DARK_THEME,\n getSpectrumColor,\n getThemeColors,\n} from \"./utils/colors\";\n\n// Types (re-export all)\nexport type {\n Spectrum,\n SpectrumType,\n Peak,\n Region,\n Annotation,\n ViewState,\n Theme,\n DisplayMode,\n Margin,\n LineStyle,\n LegendPosition,\n SpectraViewProps,\n ResolvedConfig,\n} from \"./types\";\n\nexport type { CsvParseOptions } from \"./parsers/csv\";\nexport type { PeakDetectionOptions } from \"./utils/peaks\";\nexport type {\n UseZoomPanOptions,\n UseZoomPanReturn,\n ZoomPanState,\n} from \"./hooks/useZoomPan\";\nexport type { UsePeakPickingOptions } from \"./hooks/usePeakPicking\";\nexport type { UseSpectrumDataReturn } from \"./hooks/useSpectrumData\";\nexport type { UseExportReturn } from \"./hooks/useExport\";\nexport type { CrosshairPosition, CrosshairProps } from \"./components/Crosshair/Crosshair\";\nexport type { LegendProps } from \"./components/Legend/Legend\";\nexport type { DropZoneProps } from \"./components/DropZone/DropZone\";\nexport type { AnnotationLayerProps } from \"./components/AnnotationLayer/AnnotationLayer\";\nexport type { MinimapProps } from \"./components/Minimap/Minimap\";\nexport type { TooltipProps, TooltipData } from \"./components/Tooltip/Tooltip\";\nexport type { SnapResult } from \"./utils/snap\";\nexport type { LTTBPoint } from \"./utils/lttb\";\nexport type { SnapPoint } from \"./components/Crosshair/Crosshair\";\nexport type { UseRegionSelectOptions, UseRegionSelectReturn } from \"./hooks/useRegionSelect\";\nexport type { UseKeyboardNavigationOptions } from \"./hooks/useKeyboardNavigation\";\nexport type {\n NormalizationMode,\n UseNormalizationOptions,\n UseNormalizationReturn,\n} from \"./hooks/useNormalization\";\nexport type {\n UseHistoryOptions,\n UseHistoryReturn,\n} from \"./hooks/useHistory\";\nexport type { ExportMenuProps } from \"./components/ExportMenu/ExportMenu\";\nexport type { SvgExportOptions } from \"./utils/svg-export\";\n","/**\n * SpectraView — Main interactive spectrum viewer component.\n *\n * Composes the Canvas data layer, SVG axis/annotation layer,\n * crosshair, peak markers, region selection, toolbar, and zoom/pan\n * into a single embeddable React component.\n *\n * Architecture:\n * - Canvas layer: high-performance spectral line rendering (10K+ points)\n * - SVG layer: axes, grid, annotations, crosshair (lightweight interactive elements)\n * - d3-zoom: zoom/pan math via useZoomPan hook\n */\n\nimport { useCallback, useId, useMemo, useRef, useState } from \"react\";\nimport type {\n SpectraViewProps,\n ResolvedConfig,\n Margin,\n Spectrum,\n} from \"../../types\";\nimport { computeXExtent, computeYExtent, createXScale, createYScale } from \"../../utils/scales\";\nimport { getThemeColors } from \"../../utils/colors\";\nimport { useZoomPan } from \"../../hooks/useZoomPan\";\nimport { SpectrumCanvas } from \"../SpectrumCanvas/SpectrumCanvas\";\nimport { AxisLayer } from \"../AxisLayer/AxisLayer\";\nimport { PeakMarkers } from \"../PeakMarkers/PeakMarkers\";\nimport { RegionSelector } from \"../RegionSelector/RegionSelector\";\nimport { Crosshair } from \"../Crosshair/Crosshair\";\nimport type { CrosshairPosition, SnapPoint } from \"../Crosshair/Crosshair\";\nimport { AnnotationLayer } from \"../AnnotationLayer/AnnotationLayer\";\nimport { snapToNearestSpectrum } from \"../../utils/snap\";\nimport { getSpectrumColor } from \"../../utils/colors\";\nimport { Toolbar } from \"../Toolbar/Toolbar\";\nimport { Legend } from \"../Legend/Legend\";\nimport { DropZone } from \"../DropZone/DropZone\";\nimport { StackedView } from \"../StackedView/StackedView\";\nimport { useRegionSelect } from \"../../hooks/useRegionSelect\";\nimport { useResizeObserver } from \"../../hooks/useResizeObserver\";\nimport { useKeyboardNavigation } from \"../../hooks/useKeyboardNavigation\";\nimport { generateChartDescription } from \"../../utils/a11y\";\n\n/** Default chart margins. */\nconst DEFAULT_MARGIN: Margin = {\n top: 20,\n right: 20,\n bottom: 50,\n left: 65,\n};\n\n/** Default component width. */\nconst DEFAULT_WIDTH = 800;\n\n/** Default component height. */\nconst DEFAULT_HEIGHT = 400;\n\n/**\n * Resolve user props into a complete configuration with defaults.\n */\nfunction resolveConfig(props: SpectraViewProps): ResolvedConfig {\n return {\n width: props.width ?? DEFAULT_WIDTH,\n height: props.height ?? DEFAULT_HEIGHT,\n reverseX: props.reverseX ?? false,\n showGrid: props.showGrid ?? true,\n showCrosshair: props.showCrosshair ?? true,\n showToolbar: props.showToolbar ?? true,\n showLegend: props.showLegend ?? true,\n legendPosition: props.legendPosition ?? \"bottom\",\n displayMode: props.displayMode ?? \"overlay\",\n margin: { ...DEFAULT_MARGIN, ...props.margin },\n theme: props.theme ?? \"light\",\n responsive: props.responsive ?? false,\n enableDragDrop: props.enableDragDrop ?? false,\n enableRegionSelect: props.enableRegionSelect ?? false,\n };\n}\n\n/**\n * Infer axis labels from spectrum metadata if not provided.\n */\nfunction inferLabels(\n spectra: Spectrum[],\n xLabel?: string,\n yLabel?: string,\n): { xLabel: string; yLabel: string } {\n const first = spectra[0];\n return {\n xLabel: xLabel ?? first?.xUnit ?? \"x\",\n yLabel: yLabel ?? first?.yUnit ?? \"y\",\n };\n}\n\nexport function SpectraView(props: SpectraViewProps) {\n const {\n spectra,\n peaks = [],\n regions = [],\n annotations = [],\n onPeakClick,\n onViewChange,\n onCrosshairMove,\n onToggleVisibility,\n onFileDrop,\n onRegionSelect,\n canvasRef,\n snapCrosshair = true,\n } = props;\n\n // Responsive sizing\n const { ref: resizeRef, size: measuredSize } = useResizeObserver();\n\n // Unique ID for this instance to avoid clipPath collisions (BUG-1 fix)\n const instanceId = useId();\n const clipId = `spectraview-clip-${instanceId.replace(/:/g, \"\")}`;\n\n const config = useMemo(() => resolveConfig(props), [\n props.width,\n props.height,\n props.reverseX,\n props.showGrid,\n props.showCrosshair,\n props.showToolbar,\n props.showLegend,\n props.legendPosition,\n props.displayMode,\n props.margin,\n props.theme,\n props.responsive,\n props.enableDragDrop,\n props.enableRegionSelect,\n ]);\n\n // Use measured width when responsive, fall back to configured width\n const width =\n config.responsive && measuredSize ? measuredSize.width : config.width;\n const { height, margin, reverseX, theme } = config;\n const plotWidth = width - margin.left - margin.right;\n const plotHeight = height - margin.top - margin.bottom;\n const colors = useMemo(() => getThemeColors(theme), [theme]);\n const labels = useMemo(\n () => inferLabels(spectra, props.xLabel, props.yLabel),\n [spectra, props.xLabel, props.yLabel],\n );\n\n // Compute data extents\n const xExtent = useMemo(() => computeXExtent(spectra), [spectra]);\n const yExtent = useMemo(() => computeYExtent(spectra), [spectra]);\n\n // Create base (unzoomed) scales\n const baseXScale = useMemo(\n () => createXScale(xExtent, width, margin, reverseX),\n [xExtent, width, margin, reverseX],\n );\n const baseYScale = useMemo(\n () => createYScale(yExtent, height, margin),\n [yExtent, height, margin],\n );\n\n // Stable onViewChange wrapper via ref to avoid re-attaching zoom\n const onViewChangeRef = useRef(onViewChange);\n onViewChangeRef.current = onViewChange;\n const stableOnViewChange = useMemo(\n () =>\n (xDomain: [number, number], yDomain: [number, number]) => {\n onViewChangeRef.current?.({ xDomain, yDomain });\n },\n [],\n );\n\n // Zoom/pan behavior\n const {\n zoomRef,\n state: zoomState,\n zoomedXScale,\n zoomedYScale,\n resetZoom,\n zoomIn,\n zoomOut,\n } = useZoomPan({\n plotWidth,\n plotHeight,\n xScale: baseXScale,\n yScale: baseYScale,\n onViewChange: onViewChange ? stableOnViewChange : undefined,\n });\n\n // Region selection (Shift+drag)\n const {\n pendingRegion,\n handleMouseDown: regionMouseDown,\n handleMouseMove: regionMouseMove,\n handleMouseUp: regionMouseUp,\n } = useRegionSelect({\n enabled: config.enableRegionSelect,\n xScale: zoomedXScale,\n onRegionSelect,\n });\n\n // Highlighted spectrum for legend hover\n const [highlightedId, setHighlightedId] = useState<string | null>(null);\n\n // Crosshair state — managed here so the zoom rect handles all mouse events (BUG-2 fix)\n const [crosshairPos, setCrosshairPos] = useState<CrosshairPosition | null>(null);\n const [snapPointState, setSnapPointState] = useState<SnapPoint | null>(null);\n const onCrosshairMoveRef = useRef(onCrosshairMove);\n onCrosshairMoveRef.current = onCrosshairMove;\n\n const handleMouseMove = useCallback(\n (event: React.MouseEvent<SVGRectElement>) => {\n if (!config.showCrosshair) return;\n const rect = event.currentTarget.getBoundingClientRect();\n const px = event.clientX - rect.left;\n const py = event.clientY - rect.top;\n const dataX = zoomedXScale.invert(px);\n const dataY = zoomedYScale.invert(py);\n setCrosshairPos({ px, py, dataX, dataY });\n\n // Snap to nearest spectrum data point\n if (snapCrosshair && spectra.length > 0) {\n const snap = snapToNearestSpectrum(\n spectra,\n dataX,\n py,\n zoomedXScale,\n zoomedYScale,\n );\n if (snap && snap.distance < 50) {\n const spIdx = spectra.findIndex((s) => s.id === snap.spectrumId);\n setSnapPointState({\n px: zoomedXScale(snap.x),\n py: zoomedYScale(snap.y),\n dataX: snap.x,\n dataY: snap.y,\n color: spectra[spIdx]?.color ?? getSpectrumColor(spIdx),\n });\n onCrosshairMoveRef.current?.(snap.x, snap.y);\n } else {\n setSnapPointState(null);\n onCrosshairMoveRef.current?.(dataX, dataY);\n }\n } else {\n onCrosshairMoveRef.current?.(dataX, dataY);\n }\n },\n [zoomedXScale, zoomedYScale, config.showCrosshair, snapCrosshair, spectra],\n );\n\n const handleMouseLeave = useCallback(() => {\n setCrosshairPos(null);\n setSnapPointState(null);\n }, []);\n\n // Keyboard navigation\n const handleKeyDown = useKeyboardNavigation({\n onZoomIn: zoomIn,\n onZoomOut: zoomOut,\n onReset: resetZoom,\n });\n\n // ARIA description\n const chartDescription = useMemo(\n () => generateChartDescription(spectra.length, labels.xLabel, labels.yLabel),\n [spectra.length, labels.xLabel, labels.yLabel],\n );\n\n const isStacked = config.displayMode === \"stacked\";\n\n // Empty state\n if (spectra.length === 0) {\n return (\n <div\n ref={config.responsive ? resizeRef : undefined}\n style={{\n width: config.responsive ? \"100%\" : width,\n height,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n border: `1px dashed ${colors.gridColor}`,\n borderRadius: 8,\n color: colors.tickColor,\n fontFamily: \"system-ui, sans-serif\",\n fontSize: 14,\n }}\n className={props.className}\n >\n No spectra loaded\n </div>\n );\n }\n\n const toolbarHeight = config.showToolbar ? 37 : 0;\n\n return (\n <div\n ref={config.responsive ? resizeRef : undefined}\n style={{\n width: config.responsive ? \"100%\" : width,\n background: colors.background,\n borderRadius: 4,\n overflow: \"hidden\",\n }}\n className={props.className}\n role=\"img\"\n aria-label={chartDescription}\n tabIndex={0}\n onKeyDown={handleKeyDown}\n >\n {/* Toolbar */}\n {config.showToolbar && (\n <Toolbar\n onZoomIn={zoomIn}\n onZoomOut={zoomOut}\n onReset={resetZoom}\n isZoomed={zoomState.isZoomed}\n theme={theme}\n />\n )}\n\n {/* Legend (top position) */}\n {config.showLegend && config.legendPosition === \"top\" && (\n <Legend\n spectra={spectra}\n theme={theme}\n position=\"top\"\n onToggleVisibility={onToggleVisibility}\n onHighlight={setHighlightedId}\n highlightedId={highlightedId}\n />\n )}\n\n {/* Chart area wrapped in DropZone */}\n <DropZone\n enabled={config.enableDragDrop}\n theme={theme}\n width={width}\n height={height - toolbarHeight}\n onDrop={onFileDrop}\n >\n {isStacked ? (\n /* Stacked mode: each spectrum in its own panel */\n <svg\n width={width}\n height={height - toolbarHeight}\n style={{ position: \"absolute\", top: 0, left: 0 }}\n >\n <g transform={`translate(${margin.left}, ${margin.top})`}>\n <StackedView\n spectra={spectra}\n xScale={zoomedXScale}\n plotWidth={plotWidth}\n plotHeight={plotHeight}\n margin={margin}\n theme={theme}\n showGrid={config.showGrid}\n xLabel={labels.xLabel}\n yLabel={labels.yLabel}\n />\n\n {/* Zoom/pan interaction rect */}\n <rect\n ref={zoomRef}\n x={0}\n y={0}\n width={plotWidth}\n height={plotHeight}\n fill=\"transparent\"\n style={{ cursor: \"grab\" }}\n onMouseMove={handleMouseMove}\n onMouseLeave={handleMouseLeave}\n />\n </g>\n </svg>\n ) : (\n /* Overlay mode: all spectra on single canvas */\n <>\n {/* Canvas layer for spectral data (behind SVG) */}\n <div\n style={{\n position: \"absolute\",\n top: margin.top,\n left: margin.left,\n width: plotWidth,\n height: plotHeight,\n overflow: \"hidden\",\n }}\n >\n <SpectrumCanvas\n ref={canvasRef}\n spectra={spectra}\n xScale={zoomedXScale}\n yScale={zoomedYScale}\n width={plotWidth}\n height={plotHeight}\n highlightedId={highlightedId ?? undefined}\n />\n </div>\n\n {/* SVG overlay for axes, annotations, crosshair */}\n <svg\n width={width}\n height={height - toolbarHeight}\n style={{ position: \"absolute\", top: 0, left: 0 }}\n >\n <g transform={`translate(${margin.left}, ${margin.top})`}>\n {/* Axes and grid */}\n <AxisLayer\n xScale={zoomedXScale}\n yScale={zoomedYScale}\n width={plotWidth}\n height={plotHeight}\n xLabel={labels.xLabel}\n yLabel={labels.yLabel}\n showGrid={config.showGrid}\n colors={colors}\n />\n\n {/* Clip path for plot area content */}\n <defs>\n <clipPath id={clipId}>\n <rect x={0} y={0} width={plotWidth} height={plotHeight} />\n </clipPath>\n </defs>\n\n <g clipPath={`url(#${clipId})`}>\n {/* Region highlights */}\n {regions.length > 0 && (\n <RegionSelector\n regions={regions}\n xScale={zoomedXScale}\n height={plotHeight}\n colors={colors}\n />\n )}\n\n {/* Peak markers */}\n {peaks.length > 0 && (\n <PeakMarkers\n peaks={peaks}\n xScale={zoomedXScale}\n yScale={zoomedYScale}\n colors={colors}\n onPeakClick={onPeakClick}\n />\n )}\n </g>\n\n {/* Annotations */}\n {annotations.length > 0 && (\n <AnnotationLayer\n annotations={annotations}\n xScale={zoomedXScale}\n yScale={zoomedYScale}\n colors={colors}\n />\n )}\n\n {/* Crosshair (rendered above data, pointer-events: none) */}\n {config.showCrosshair && (\n <Crosshair\n position={crosshairPos}\n width={plotWidth}\n height={plotHeight}\n colors={colors}\n snapPoint={snapPointState}\n />\n )}\n\n {/* Pending region highlight */}\n {pendingRegion && (\n <rect\n x={zoomedXScale(pendingRegion.xStart)}\n y={0}\n width={Math.abs(\n zoomedXScale(pendingRegion.xEnd) -\n zoomedXScale(pendingRegion.xStart),\n )}\n height={plotHeight}\n fill={colors.regionFill}\n stroke={colors.regionStroke}\n strokeWidth={1}\n pointerEvents=\"none\"\n />\n )}\n\n {/* Zoom/pan + crosshair interaction rect */}\n <rect\n ref={zoomRef}\n x={0}\n y={0}\n width={plotWidth}\n height={plotHeight}\n fill=\"transparent\"\n style={{ cursor: config.showCrosshair ? \"crosshair\" : \"grab\" }}\n onMouseDown={regionMouseDown}\n onMouseMove={(e) => {\n handleMouseMove(e);\n regionMouseMove(e);\n }}\n onMouseUp={regionMouseUp}\n onMouseLeave={handleMouseLeave}\n />\n </g>\n </svg>\n </>\n )}\n </DropZone>\n\n {/* Legend (bottom position) */}\n {config.showLegend && config.legendPosition === \"bottom\" && (\n <Legend\n spectra={spectra}\n theme={theme}\n position=\"bottom\"\n onToggleVisibility={onToggleVisibility}\n onHighlight={setHighlightedId}\n highlightedId={highlightedId}\n />\n )}\n </div>\n );\n}\n","/**\n * D3 scale factories for spectral axes.\n *\n * Handles reversed x-axis (standard for IR wavenumber display)\n * and automatic domain computation from spectral data.\n */\n\nimport { scaleLinear } from \"d3-scale\";\nimport { extent } from \"d3-array\";\nimport type { Spectrum, Margin } from \"../types\";\n\n/** Padding factor applied to y-axis domain (5% on each side). */\nconst Y_PADDING = 0.05;\n\n/**\n * Compute the x-axis extent across all visible spectra.\n */\nexport function computeXExtent(spectra: Spectrum[]): [number, number] {\n let globalMin = Infinity;\n let globalMax = -Infinity;\n\n for (const s of spectra) {\n if (s.visible === false) continue;\n const [min, max] = extent(s.x as number[]) as [number, number];\n if (min < globalMin) globalMin = min;\n if (max > globalMax) globalMax = max;\n }\n\n if (!isFinite(globalMin)) return [0, 1];\n return [globalMin, globalMax];\n}\n\n/**\n * Compute the y-axis extent across all visible spectra with padding.\n */\nexport function computeYExtent(spectra: Spectrum[]): [number, number] {\n let globalMin = Infinity;\n let globalMax = -Infinity;\n\n for (const s of spectra) {\n if (s.visible === false) continue;\n const [min, max] = extent(s.y as number[]) as [number, number];\n if (min < globalMin) globalMin = min;\n if (max > globalMax) globalMax = max;\n }\n\n if (!isFinite(globalMin)) return [0, 1];\n\n const range = globalMax - globalMin;\n const pad = range * Y_PADDING;\n return [globalMin - pad, globalMax + pad];\n}\n\n/**\n * Create an x-axis scale.\n *\n * When `reverseX` is true, the domain is reversed so higher wavenumbers\n * appear on the left (standard IR convention).\n */\nexport function createXScale(\n domain: [number, number],\n width: number,\n margin: Margin,\n reverseX: boolean,\n) {\n const plotWidth = width - margin.left - margin.right;\n const d = reverseX ? [domain[1], domain[0]] : domain;\n return scaleLinear().domain(d).range([0, plotWidth]);\n}\n\n/**\n * Create a y-axis scale (always low values at bottom, high at top).\n */\nexport function createYScale(\n domain: [number, number],\n height: number,\n margin: Margin,\n) {\n const plotHeight = height - margin.top - margin.bottom;\n return scaleLinear().domain(domain).range([plotHeight, 0]);\n}\n","/**\n * Default color palette for rendering multiple spectra.\n *\n * Colors are chosen for good contrast on both light and dark backgrounds,\n * and are distinguishable for common forms of color blindness.\n */\n\n/** Default spectrum color palette (10 colors). */\nexport const SPECTRUM_COLORS = [\n \"#2563eb\", // blue\n \"#dc2626\", // red\n \"#16a34a\", // green\n \"#9333ea\", // purple\n \"#ea580c\", // orange\n \"#0891b2\", // cyan\n \"#be185d\", // pink\n \"#854d0e\", // brown\n \"#4f46e5\", // indigo\n \"#65a30d\", // lime\n] as const;\n\n/** Theme color definition. */\nexport interface ThemeColors {\n background: string;\n axisColor: string;\n gridColor: string;\n tickColor: string;\n labelColor: string;\n crosshairColor: string;\n regionFill: string;\n regionStroke: string;\n tooltipBg: string;\n tooltipBorder: string;\n tooltipText: string;\n}\n\n/** Light theme colors. */\nexport const LIGHT_THEME: ThemeColors = {\n background: \"#ffffff\",\n axisColor: \"#374151\",\n gridColor: \"#e5e7eb\",\n tickColor: \"#6b7280\",\n labelColor: \"#111827\",\n crosshairColor: \"#9ca3af\",\n regionFill: \"rgba(37, 99, 235, 0.1)\",\n regionStroke: \"rgba(37, 99, 235, 0.4)\",\n tooltipBg: \"#ffffff\",\n tooltipBorder: \"#d1d5db\",\n tooltipText: \"#111827\",\n};\n\n/** Dark theme colors. */\nexport const DARK_THEME: ThemeColors = {\n background: \"#111827\",\n axisColor: \"#d1d5db\",\n gridColor: \"#374151\",\n tickColor: \"#9ca3af\",\n labelColor: \"#f9fafb\",\n crosshairColor: \"#6b7280\",\n regionFill: \"rgba(96, 165, 250, 0.15)\",\n regionStroke: \"rgba(96, 165, 250, 0.5)\",\n tooltipBg: \"#1f2937\",\n tooltipBorder: \"#4b5563\",\n tooltipText: \"#f9fafb\",\n};\n\n/**\n * Get the color for a spectrum at the given index.\n *\n * Cycles through the palette if index exceeds palette length.\n */\nexport function getSpectrumColor(index: number): string {\n return SPECTRUM_COLORS[index % SPECTRUM_COLORS.length];\n}\n\n/**\n * Get theme colors for the given theme name.\n */\nexport function getThemeColors(theme: \"light\" | \"dark\"): ThemeColors {\n return theme === \"dark\" ? DARK_THEME : LIGHT_THEME;\n}\n","/**\n * Hook for zoom and pan behavior backed by d3-zoom.\n *\n * Provides smooth mouse wheel zoom, click-drag pan, and double-click\n * reset. Works with both the SVG overlay and Canvas data layers.\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { zoom, type ZoomBehavior, zoomIdentity, type ZoomTransform } from \"d3-zoom\";\nimport { select } from \"d3-selection\";\nimport \"d3-transition\";\nimport type { ScaleLinear } from \"d3-scale\";\n\nexport interface ZoomPanState {\n /** Current d3 zoom transform. */\n transform: ZoomTransform;\n /** Whether the view is currently zoomed (not at identity). */\n isZoomed: boolean;\n}\n\nexport interface UseZoomPanOptions {\n /** Width of the plot area (excluding margins). */\n plotWidth: number;\n /** Height of the plot area (excluding margins). */\n plotHeight: number;\n /** Base x-scale (unzoomed). */\n xScale: ScaleLinear<number, number>;\n /** Base y-scale (unzoomed). */\n yScale: ScaleLinear<number, number>;\n /** Maximum zoom factor. */\n scaleExtent?: [number, number];\n /** Whether zoom/pan is enabled. */\n enabled?: boolean;\n /** Callback when the view changes. */\n onViewChange?: (xDomain: [number, number], yDomain: [number, number]) => void;\n}\n\nexport interface UseZoomPanReturn {\n /** Ref to attach to the interaction overlay element. */\n zoomRef: React.RefObject<SVGRectElement | null>;\n /** Current zoom/pan state. */\n state: ZoomPanState;\n /** Zoomed (rescaled) x-scale. */\n zoomedXScale: ScaleLinear<number, number>;\n /** Zoomed (rescaled) y-scale. */\n zoomedYScale: ScaleLinear<number, number>;\n /** Reset zoom to initial view. */\n resetZoom: () => void;\n /** Zoom in by a fixed step. */\n zoomIn: () => void;\n /** Zoom out by a fixed step. */\n zoomOut: () => void;\n}\n\n/** Zoom step multiplier for zoomIn/zoomOut. */\nconst ZOOM_STEP = 1.5;\n\nexport function useZoomPan(options: UseZoomPanOptions): UseZoomPanReturn {\n const {\n plotWidth,\n plotHeight,\n xScale,\n yScale,\n scaleExtent = [1, 50],\n enabled = true,\n onViewChange,\n } = options;\n\n const zoomRef = useRef<SVGRectElement | null>(null);\n const zoomBehaviorRef = useRef<ZoomBehavior<SVGRectElement, unknown> | null>(null);\n\n // Store callbacks and config in refs to avoid effect re-runs (REACT-2 fix)\n const onViewChangeRef = useRef(onViewChange);\n onViewChangeRef.current = onViewChange;\n const scaleExtentRef = useRef(scaleExtent);\n scaleExtentRef.current = scaleExtent;\n\n const [transform, setTransform] = useState<ZoomTransform>(zoomIdentity);\n\n // Memoize rescaled axes from the current transform (BUG-4 fix)\n const zoomedXScale = useMemo(\n () => transform.rescaleX(xScale.copy()),\n [transform, xScale],\n );\n const zoomedYScale = useMemo(\n () => transform.rescaleY(yScale.copy()),\n [transform, yScale],\n );\n\n // Set up d3-zoom behavior\n useEffect(() => {\n const element = zoomRef.current;\n if (!element || !enabled) return;\n\n const zoomBehavior = zoom<SVGRectElement, unknown>()\n .scaleExtent(scaleExtentRef.current)\n .extent([\n [0, 0],\n [plotWidth, plotHeight],\n ])\n .translateExtent([\n [-Infinity, -Infinity],\n [Infinity, Infinity],\n ])\n .on(\"zoom\", (event) => {\n const newTransform = event.transform as ZoomTransform;\n setTransform(newTransform);\n\n if (onViewChangeRef.current) {\n const newXScale = newTransform.rescaleX(xScale.copy());\n const newYScale = newTransform.rescaleY(yScale.copy());\n onViewChangeRef.current(\n newXScale.domain() as [number, number],\n newYScale.domain() as [number, number],\n );\n }\n });\n\n zoomBehaviorRef.current = zoomBehavior;\n\n select(element).call(zoomBehavior);\n\n // Double-click to reset\n select(element).on(\"dblclick.zoom\", () => {\n select(element).transition().duration(300).call(zoomBehavior.transform, zoomIdentity);\n });\n\n // Stale ref fix: use captured `element` instead of zoomRef.current\n return () => {\n select(element).on(\".zoom\", null);\n };\n }, [plotWidth, plotHeight, enabled, xScale, yScale]);\n\n const resetZoom = useCallback(() => {\n if (!zoomRef.current || !zoomBehaviorRef.current) return;\n select(zoomRef.current)\n .transition()\n .duration(300)\n .call(zoomBehaviorRef.current.transform, zoomIdentity);\n }, []);\n\n const zoomIn = useCallback(() => {\n if (!zoomRef.current || !zoomBehaviorRef.current) return;\n select(zoomRef.current)\n .transition()\n .duration(200)\n .call(zoomBehaviorRef.current.scaleBy, ZOOM_STEP);\n }, []);\n\n const zoomOut = useCallback(() => {\n if (!zoomRef.current || !zoomBehaviorRef.current) return;\n select(zoomRef.current)\n .transition()\n .duration(200)\n .call(zoomBehaviorRef.current.scaleBy, 1 / ZOOM_STEP);\n }, []);\n\n return {\n zoomRef,\n state: {\n transform,\n isZoomed: transform.k !== 1 || transform.x !== 0 || transform.y !== 0,\n },\n zoomedXScale,\n zoomedYScale,\n resetZoom,\n zoomIn,\n zoomOut,\n };\n}\n","/**\n * Canvas rendering layer for spectral data.\n *\n * Uses HTML5 Canvas 2D for high-performance rendering of spectral lines.\n * Redraws on zoom/pan transform changes and spectrum data changes.\n */\n\nimport { forwardRef, useEffect, useImperativeHandle, useRef } from \"react\";\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { Spectrum } from \"../../types\";\nimport { drawAllSpectra } from \"../../utils/rendering\";\n\nexport interface SpectrumCanvasProps {\n /** Spectra to render. */\n spectra: Spectrum[];\n /** X-axis scale (already zoomed). */\n xScale: ScaleLinear<number, number>;\n /** Y-axis scale (already zoomed). */\n yScale: ScaleLinear<number, number>;\n /** Canvas width in pixels. */\n width: number;\n /** Canvas height in pixels. */\n height: number;\n /** ID of the currently highlighted spectrum. */\n highlightedId?: string;\n}\n\nexport const SpectrumCanvas = forwardRef<HTMLCanvasElement, SpectrumCanvasProps>(\n function SpectrumCanvas(\n { spectra, xScale, yScale, width, height, highlightedId },\n ref,\n ) {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const dprRef = useRef(1);\n\n // Expose the internal canvas ref to parent via forwarded ref\n useImperativeHandle(ref, () => canvasRef.current!, []);\n\n // Set up canvas DPR only when dimensions change (avoids flicker on zoom)\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const dpr = window.devicePixelRatio || 1;\n dprRef.current = dpr;\n canvas.width = width * dpr;\n canvas.height = height * dpr;\n }, [width, height]);\n\n // Redraw spectra when data or scales change\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return;\n\n const dpr = dprRef.current;\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\n drawAllSpectra(ctx, spectra, xScale, yScale, width, height, highlightedId);\n }, [spectra, xScale, yScale, width, height, highlightedId]);\n\n return (\n <canvas\n ref={canvasRef}\n style={{\n width,\n height,\n position: \"absolute\",\n top: 0,\n left: 0,\n pointerEvents: \"none\",\n }}\n />\n );\n },\n);\n","/**\n * Largest-Triangle-Three-Buckets (LTTB) downsampling algorithm.\n *\n * LTTB produces visually superior downsampled representations compared to\n * simple min-max binning. It works by dividing data into buckets and\n * selecting the point in each bucket that forms the largest triangle with\n * the selected points in adjacent buckets.\n *\n * Reference: Sveinn Steinarsson, \"Downsampling Time Series for Visual\n * Representation\" (2013).\n *\n * @module lttb\n */\n\n/** A downsampled point with its original index. */\nexport interface LTTBPoint {\n /** Pixel x coordinate. */\n px: number;\n /** Pixel y coordinate. */\n py: number;\n /** Original index in the source arrays. */\n index: number;\n}\n\n/**\n * Downsample data using the LTTB algorithm.\n *\n * @param x - Source x-values\n * @param y - Source y-values\n * @param startIdx - Start index (inclusive) in the source arrays\n * @param endIdx - End index (exclusive) in the source arrays\n * @param xScale - Function mapping data x to pixel x\n * @param yScale - Function mapping data y to pixel y\n * @param targetCount - Desired number of output points\n * @returns Array of downsampled points\n */\nexport function lttbDownsample(\n x: Float64Array | number[],\n y: Float64Array | number[],\n startIdx: number,\n endIdx: number,\n xScale: (v: number) => number,\n yScale: (v: number) => number,\n targetCount: number,\n): LTTBPoint[] {\n const n = endIdx - startIdx;\n\n // If fewer points than target, return all\n if (n <= targetCount) {\n const result: LTTBPoint[] = [];\n for (let i = startIdx; i < endIdx; i++) {\n result.push({\n px: xScale(x[i] as number),\n py: yScale(y[i] as number),\n index: i,\n });\n }\n return result;\n }\n\n // Always include first and last points\n const result: LTTBPoint[] = [];\n result.push({\n px: xScale(x[startIdx] as number),\n py: yScale(y[startIdx] as number),\n index: startIdx,\n });\n\n // Number of buckets (excluding first and last points)\n const bucketCount = targetCount - 2;\n const bucketSize = (n - 2) / bucketCount;\n\n let prevSelectedIdx = startIdx;\n\n for (let bucket = 0; bucket < bucketCount; bucket++) {\n // Current bucket range\n const bucketStart = startIdx + 1 + Math.floor(bucket * bucketSize);\n const bucketEnd = startIdx + 1 + Math.min(\n Math.floor((bucket + 1) * bucketSize),\n n - 2,\n );\n\n // Next bucket average (used as the third vertex of the triangle)\n const nextBucketStart = bucketEnd;\n const nextBucketEnd = startIdx + 1 + Math.min(\n Math.floor((bucket + 2) * bucketSize),\n n - 2,\n );\n // For the last bucket, use the last data point as the average\n let avgX: number;\n let avgY: number;\n\n if (bucket === bucketCount - 1) {\n avgX = xScale(x[endIdx - 1] as number);\n avgY = yScale(y[endIdx - 1] as number);\n } else {\n avgX = 0;\n avgY = 0;\n const avgCount = nextBucketEnd - nextBucketStart;\n for (let i = nextBucketStart; i < nextBucketEnd; i++) {\n avgX += xScale(x[i] as number);\n avgY += yScale(y[i] as number);\n }\n if (avgCount > 0) {\n avgX /= avgCount;\n avgY /= avgCount;\n }\n }\n\n // Previous selected point (first vertex)\n const prevPx = xScale(x[prevSelectedIdx] as number);\n const prevPy = yScale(y[prevSelectedIdx] as number);\n\n // Find point in current bucket with maximum triangle area\n let maxArea = -1;\n let bestIdx = bucketStart;\n\n for (let i = bucketStart; i < bucketEnd; i++) {\n const px = xScale(x[i] as number);\n const py = yScale(y[i] as number);\n\n // Triangle area (using cross product formula, no /2 needed for comparison)\n const area = Math.abs(\n (prevPx - avgX) * (py - prevPy) -\n (prevPx - px) * (avgY - prevPy),\n );\n\n if (area > maxArea) {\n maxArea = area;\n bestIdx = i;\n }\n }\n\n result.push({\n px: xScale(x[bestIdx] as number),\n py: yScale(y[bestIdx] as number),\n index: bestIdx,\n });\n prevSelectedIdx = bestIdx;\n }\n\n // Add last point\n result.push({\n px: xScale(x[endIdx - 1] as number),\n py: yScale(y[endIdx - 1] as number),\n index: endIdx - 1,\n });\n\n return result;\n}\n","/**\n * Canvas 2D rendering utilities for drawing spectral lines.\n *\n * Canvas is used for the data-heavy spectral line rendering (10K+ points)\n * while SVG handles axes, annotations, and interactive overlays.\n */\n\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { Spectrum, LineStyle } from \"../types\";\nimport { getSpectrumColor } from \"./colors\";\nimport { lttbDownsample } from \"./lttb\";\n\n/** Default line width for spectrum rendering. */\nconst LINE_WIDTH = 1.5;\n\n/** Canvas dash patterns for line styles. */\nconst CANVAS_DASH_PATTERNS: Record<LineStyle, number[]> = {\n solid: [],\n dashed: [8, 4],\n dotted: [2, 2],\n \"dash-dot\": [8, 4, 2, 4],\n};\n\n/**\n * Threshold: if visible points exceed this count, apply LTTB downsampling.\n * Target output is plotWidth * 2 points — enough visual fidelity for\n * sub-pixel accuracy while dramatically reducing draw calls.\n */\nconst DECIMATION_THRESHOLD = 2000;\n\n/**\n * Clear the canvas and set up for drawing.\n */\nexport function clearCanvas(\n ctx: CanvasRenderingContext2D,\n width: number,\n height: number,\n): void {\n ctx.clearRect(0, 0, width, height);\n}\n\n/**\n * Draw a single spectrum as a line path on the canvas.\n *\n * Uses beginPath/lineTo for maximum performance with large point counts.\n * Applies min-max decimation when point count exceeds DECIMATION_THRESHOLD.\n */\nexport function drawSpectrum(\n ctx: CanvasRenderingContext2D,\n spectrum: Spectrum,\n index: number,\n xScale: ScaleLinear<number, number>,\n yScale: ScaleLinear<number, number>,\n plotWidth: number,\n options?: {\n highlighted?: boolean;\n opacity?: number;\n },\n): void {\n const { highlighted = false, opacity = 1.0 } = options ?? {};\n const n = Math.min(spectrum.x.length, spectrum.y.length);\n if (n < 2) return;\n\n const color = spectrum.color ?? getSpectrumColor(index);\n const baseWidth = spectrum.lineWidth ?? LINE_WIDTH;\n const lineWidth = highlighted ? baseWidth + 1 : baseWidth;\n const dashPattern = CANVAS_DASH_PATTERNS[spectrum.lineStyle ?? \"solid\"] ?? [];\n\n // Get visible x-domain for culling\n const [xMin, xMax] = xScale.domain() as [number, number];\n const domainMin = Math.min(xMin, xMax);\n const domainMax = Math.max(xMin, xMax);\n\n // Find range of visible indices (with 1-point margin for continuity)\n let startIdx = 0;\n let endIdx = n;\n for (let i = 0; i < n; i++) {\n if ((spectrum.x[i] as number) >= domainMin || (i < n - 1 && (spectrum.x[i + 1] as number) >= domainMin)) {\n startIdx = Math.max(0, i - 1);\n break;\n }\n }\n for (let i = n - 1; i >= 0; i--) {\n if ((spectrum.x[i] as number) <= domainMax || (i > 0 && (spectrum.x[i - 1] as number) <= domainMax)) {\n endIdx = Math.min(n, i + 2);\n break;\n }\n }\n\n const visibleCount = endIdx - startIdx;\n\n ctx.save();\n ctx.beginPath();\n ctx.strokeStyle = color;\n ctx.lineWidth = lineWidth;\n ctx.globalAlpha = opacity;\n ctx.lineJoin = \"round\";\n ctx.setLineDash(dashPattern);\n\n if (visibleCount > DECIMATION_THRESHOLD) {\n // LTTB downsampled path: visually optimal point selection\n const targetPoints = Math.max(Math.ceil(plotWidth * 2), 200);\n const points = lttbDownsample(spectrum.x, spectrum.y, startIdx, endIdx, xScale, yScale, targetPoints);\n if (points.length > 0) {\n ctx.moveTo(points[0].px, points[0].py);\n for (let i = 1; i < points.length; i++) {\n ctx.lineTo(points[i].px, points[i].py);\n }\n }\n } else {\n // Direct path: draw all visible points\n let started = false;\n for (let i = startIdx; i < endIdx; i++) {\n const px = xScale(spectrum.x[i] as number);\n const py = yScale(spectrum.y[i] as number);\n if (!started) {\n ctx.moveTo(px, py);\n started = true;\n } else {\n ctx.lineTo(px, py);\n }\n }\n }\n\n ctx.stroke();\n ctx.restore();\n}\n\n/**\n * Draw all visible spectra onto the canvas.\n */\nexport function drawAllSpectra(\n ctx: CanvasRenderingContext2D,\n spectra: Spectrum[],\n xScale: ScaleLinear<number, number>,\n yScale: ScaleLinear<number, number>,\n width: number,\n height: number,\n highlightedId?: string,\n): void {\n clearCanvas(ctx, width, height);\n\n spectra.forEach((spectrum, index) => {\n if (spectrum.visible === false) return;\n\n drawSpectrum(ctx, spectrum, index, xScale, yScale, width, {\n highlighted: spectrum.id === highlightedId,\n opacity: highlightedId && spectrum.id !== highlightedId ? 0.3 : 1.0,\n });\n });\n}\n","/**\n * SVG axis layer rendering X and Y axes with labels and grid lines.\n *\n * Built with plain SVG for minimal bundle size. Handles reversed x-axis\n * (standard for IR wavenumber display).\n */\n\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { ThemeColors } from \"../../utils/colors\";\n\nexport interface AxisLayerProps {\n /** X-axis scale (already zoomed). */\n xScale: ScaleLinear<number, number>;\n /** Y-axis scale (already zoomed). */\n yScale: ScaleLinear<number, number>;\n /** Plot area width (excluding margins). */\n width: number;\n /** Plot area height (excluding margins). */\n height: number;\n /** X-axis label. */\n xLabel?: string;\n /** Y-axis label. */\n yLabel?: string;\n /** Show grid lines. */\n showGrid?: boolean;\n /** Theme colors. */\n colors: ThemeColors;\n}\n\n/** Number of tick marks to show on each axis. */\nconst TICK_COUNT = 8;\n\n/**\n * Generate evenly-spaced tick values for a linear scale.\n */\nfunction generateTicks(scale: ScaleLinear<number, number>, count: number): number[] {\n const [d0, d1] = scale.domain() as [number, number];\n const min = Math.min(d0, d1);\n const max = Math.max(d0, d1);\n const step = (max - min) / (count - 1);\n return Array.from({ length: count }, (_, i) => min + i * step);\n}\n\n/**\n * Format a tick value for display.\n */\nfunction formatTick(value: number): string {\n if (Math.abs(value) >= 1000) return Math.round(value).toString();\n if (Math.abs(value) >= 1) return value.toFixed(1);\n if (Math.abs(value) >= 0.01) return value.toFixed(3);\n return value.toExponential(1);\n}\n\nexport function AxisLayer({\n xScale,\n yScale,\n width,\n height,\n xLabel,\n yLabel,\n showGrid = true,\n colors,\n}: AxisLayerProps) {\n const xTicks = generateTicks(xScale, TICK_COUNT);\n const yTicks = generateTicks(yScale, TICK_COUNT - 2);\n\n return (\n <g>\n {/* Grid lines */}\n {showGrid && (\n <g>\n {xTicks.map((tick) => (\n <line\n key={`xgrid-${tick}`}\n x1={xScale(tick)}\n x2={xScale(tick)}\n y1={0}\n y2={height}\n stroke={colors.gridColor}\n strokeWidth={0.5}\n />\n ))}\n {yTicks.map((tick) => (\n <line\n key={`ygrid-${tick}`}\n x1={0}\n x2={width}\n y1={yScale(tick)}\n y2={yScale(tick)}\n stroke={colors.gridColor}\n strokeWidth={0.5}\n />\n ))}\n </g>\n )}\n\n {/* X-axis */}\n <g transform={`translate(0, ${height})`}>\n <line x1={0} x2={width} y1={0} y2={0} stroke={colors.axisColor} />\n {xTicks.map((tick) => (\n <g key={`xtick-${tick}`} transform={`translate(${xScale(tick)}, 0)`}>\n <line y1={0} y2={6} stroke={colors.axisColor} />\n <text\n y={20}\n textAnchor=\"middle\"\n fill={colors.tickColor}\n fontSize={11}\n fontFamily=\"system-ui, sans-serif\"\n >\n {formatTick(tick)}\n </text>\n </g>\n ))}\n {xLabel && (\n <text\n x={width / 2}\n y={42}\n textAnchor=\"middle\"\n fill={colors.labelColor}\n fontSize={13}\n fontFamily=\"system-ui, sans-serif\"\n >\n {xLabel}\n </text>\n )}\n </g>\n\n {/* Y-axis */}\n <g>\n <line x1={0} x2={0} y1={0} y2={height} stroke={colors.axisColor} />\n {yTicks.map((tick) => (\n <g key={`ytick-${tick}`} transform={`translate(0, ${yScale(tick)})`}>\n <line x1={-6} x2={0} stroke={colors.axisColor} />\n <text\n x={-10}\n textAnchor=\"end\"\n dominantBaseline=\"middle\"\n fill={colors.tickColor}\n fontSize={11}\n fontFamily=\"system-ui, sans-serif\"\n >\n {formatTick(tick)}\n </text>\n </g>\n ))}\n {yLabel && (\n <text\n transform={`translate(-50, ${height / 2}) rotate(-90)`}\n textAnchor=\"middle\"\n fill={colors.labelColor}\n fontSize={13}\n fontFamily=\"system-ui, sans-serif\"\n >\n {yLabel}\n </text>\n )}\n </g>\n </g>\n );\n}\n","/**\n * Peak annotation markers rendered as SVG overlays.\n *\n * Displays small triangles at peak positions with wavenumber labels.\n * Supports click interaction for peak selection.\n */\n\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { Peak } from \"../../types\";\nimport type { ThemeColors } from \"../../utils/colors\";\n\nexport interface PeakMarkersProps {\n /** Peaks to display. */\n peaks: Peak[];\n /** X-axis scale (zoomed). */\n xScale: ScaleLinear<number, number>;\n /** Y-axis scale (zoomed). */\n yScale: ScaleLinear<number, number>;\n /** Theme colors. */\n colors: ThemeColors;\n /** Callback when a peak is clicked. */\n onPeakClick?: (peak: Peak) => void;\n}\n\n/** Size of the peak marker triangle. */\nconst MARKER_SIZE = 5;\n\n/** Vertical offset for the label above the marker. */\nconst LABEL_OFFSET = 14;\n\nexport function PeakMarkers({\n peaks,\n xScale,\n yScale,\n colors,\n onPeakClick,\n}: PeakMarkersProps) {\n // Get visible domain to cull off-screen peaks\n const [xMin, xMax] = xScale.domain() as [number, number];\n const domainMin = Math.min(xMin, xMax);\n const domainMax = Math.max(xMin, xMax);\n\n const visiblePeaks = peaks.filter(\n (p) => p.x >= domainMin && p.x <= domainMax,\n );\n\n return (\n <g className=\"spectraview-peaks\">\n {visiblePeaks.map((peak, i) => {\n const px = xScale(peak.x);\n const py = yScale(peak.y);\n\n return (\n <g\n key={`peak-${peak.x}-${i}`}\n transform={`translate(${px}, ${py})`}\n style={{ cursor: onPeakClick ? \"pointer\" : \"default\" }}\n onClick={() => onPeakClick?.(peak)}\n >\n {/* Triangle marker pointing down */}\n <polygon\n points={`0,${-MARKER_SIZE} ${-MARKER_SIZE},${-MARKER_SIZE * 2.5} ${MARKER_SIZE},${-MARKER_SIZE * 2.5}`}\n fill={colors.labelColor}\n opacity={0.8}\n />\n {/* Wavenumber label */}\n {peak.label && (\n <text\n y={-MARKER_SIZE * 2.5 - LABEL_OFFSET}\n textAnchor=\"middle\"\n fill={colors.labelColor}\n fontSize={10}\n fontFamily=\"system-ui, sans-serif\"\n fontWeight={500}\n >\n {peak.label}\n </text>\n )}\n </g>\n );\n })}\n </g>\n );\n}\n","/**\n * Region selection overlay for click-drag x-axis region selection.\n *\n * Renders highlighted rectangular regions on the spectrum and\n * handles mouse interaction for creating new regions.\n */\n\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { Region } from \"../../types\";\nimport type { ThemeColors } from \"../../utils/colors\";\n\nexport interface RegionSelectorProps {\n /** Existing regions to display. */\n regions: Region[];\n /** X-axis scale (zoomed). */\n xScale: ScaleLinear<number, number>;\n /** Plot area height. */\n height: number;\n /** Theme colors. */\n colors: ThemeColors;\n}\n\nexport function RegionSelector({\n regions,\n xScale,\n height,\n colors,\n}: RegionSelectorProps) {\n return (\n <g className=\"spectraview-regions\">\n {regions.map((region, i) => {\n const x1 = xScale(region.xStart);\n const x2 = xScale(region.xEnd);\n const left = Math.min(x1, x2);\n const w = Math.abs(x2 - x1);\n\n return (\n <g key={`region-${i}`}>\n <rect\n x={left}\n y={0}\n width={w}\n height={height}\n fill={region.color ?? colors.regionFill}\n stroke={colors.regionStroke}\n strokeWidth={1}\n />\n {region.label && (\n <text\n x={left + w / 2}\n y={12}\n textAnchor=\"middle\"\n fill={colors.labelColor}\n fontSize={10}\n fontFamily=\"system-ui, sans-serif\"\n >\n {region.label}\n </text>\n )}\n </g>\n );\n })}\n </g>\n );\n}\n","/**\n * Crosshair overlay showing current cursor position with coordinate readout.\n *\n * Pure rendering component — mouse tracking is handled by the parent\n * SpectraView component on the shared zoom/interaction rect.\n */\n\nimport type { ThemeColors } from \"../../utils/colors\";\n\n/** Position data for the crosshair. */\nexport interface CrosshairPosition {\n /** Pixel x coordinate within the plot area. */\n px: number;\n /** Pixel y coordinate within the plot area. */\n py: number;\n /** Data-space x value. */\n dataX: number;\n /** Data-space y value. */\n dataY: number;\n}\n\n/** Snap point data for rendering a snap dot on the nearest spectrum. */\nexport interface SnapPoint {\n /** Pixel x of the snapped data point. */\n px: number;\n /** Pixel y of the snapped data point. */\n py: number;\n /** Data-space x value. */\n dataX: number;\n /** Data-space y value. */\n dataY: number;\n /** Color of the spectrum (for dot fill). */\n color?: string;\n}\n\nexport interface CrosshairProps {\n /** Current crosshair position, or null when not hovering. */\n position: CrosshairPosition | null;\n /** Plot area width. */\n width: number;\n /** Plot area height. */\n height: number;\n /** Theme colors. */\n colors: ThemeColors;\n /** Optional snap point on nearest spectrum. */\n snapPoint?: SnapPoint | null;\n}\n\nexport function Crosshair({\n position,\n width,\n height,\n colors,\n snapPoint,\n}: CrosshairProps) {\n if (!position) return null;\n\n return (\n <g className=\"spectraview-crosshair\" pointerEvents=\"none\">\n {/* Vertical line */}\n <line\n x1={position.px}\n x2={position.px}\n y1={0}\n y2={height}\n stroke={colors.crosshairColor}\n strokeWidth={1}\n strokeDasharray=\"4 4\"\n />\n {/* Horizontal line */}\n <line\n x1={0}\n x2={width}\n y1={position.py}\n y2={position.py}\n stroke={colors.crosshairColor}\n strokeWidth={1}\n strokeDasharray=\"4 4\"\n />\n {/* Snap dot on nearest spectrum */}\n {snapPoint && (\n <circle\n cx={snapPoint.px}\n cy={snapPoint.py}\n r={4}\n fill={snapPoint.color ?? colors.crosshairColor}\n stroke={colors.background}\n strokeWidth={1.5}\n />\n )}\n\n {/* Coordinate readout (shows snapped values when available) */}\n <g\n transform={`translate(${Math.min(position.px + 10, width - 100)}, ${Math.max(position.py - 10, 20)})`}\n >\n <rect\n x={0}\n y={-14}\n width={90}\n height={18}\n rx={3}\n fill={colors.tooltipBg}\n stroke={colors.tooltipBorder}\n strokeWidth={0.5}\n opacity={0.9}\n />\n <text\n x={5}\n y={0}\n fill={colors.tooltipText}\n fontSize={10}\n fontFamily=\"monospace\"\n >\n {formatValue(snapPoint?.dataX ?? position.dataX)},{\" \"}\n {formatValue(snapPoint?.dataY ?? position.dataY)}\n </text>\n </g>\n </g>\n );\n}\n\nfunction formatValue(v: number): string {\n if (Math.abs(v) >= 100) return Math.round(v).toString();\n if (Math.abs(v) >= 1) return v.toFixed(1);\n return v.toFixed(4);\n}\n","/**\n * AnnotationLayer — Renders text annotations at specified data positions.\n *\n * Annotations are positioned in data-space and transformed to pixel-space\n * via the provided scales. Supports optional anchor lines from the\n * annotation text to the data point.\n */\n\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { Annotation } from \"../../types\";\nimport type { ThemeColors } from \"../../utils/colors\";\n\nexport interface AnnotationLayerProps {\n /** Annotations to render. */\n annotations: Annotation[];\n /** X scale (zoomed). */\n xScale: ScaleLinear<number, number>;\n /** Y scale (zoomed). */\n yScale: ScaleLinear<number, number>;\n /** Theme colors. */\n colors: ThemeColors;\n}\n\nexport function AnnotationLayer({\n annotations,\n xScale,\n yScale,\n colors,\n}: AnnotationLayerProps) {\n if (annotations.length === 0) return null;\n\n return (\n <g className=\"spectraview-annotations\" pointerEvents=\"none\">\n {annotations.map((ann) => {\n const px = xScale(ann.x);\n const py = yScale(ann.y);\n const [dx, dy] = ann.offset ?? [0, -20];\n const textX = px + dx;\n const textY = py + dy;\n const fontSize = ann.fontSize ?? 11;\n const color = ann.color ?? colors.tickColor;\n const showLine = ann.showAnchorLine !== false;\n\n return (\n <g key={ann.id}>\n {/* Anchor line */}\n {showLine && (\n <line\n x1={px}\n y1={py}\n x2={textX}\n y2={textY}\n stroke={color}\n strokeWidth={0.75}\n strokeDasharray=\"3 2\"\n opacity={0.6}\n />\n )}\n {/* Anchor dot */}\n <circle cx={px} cy={py} r={2.5} fill={color} opacity={0.8} />\n {/* Text background for readability */}\n <text\n x={textX}\n y={textY}\n fill={colors.background}\n fontSize={fontSize}\n fontFamily=\"system-ui, sans-serif\"\n textAnchor=\"middle\"\n dominantBaseline=\"auto\"\n stroke={colors.background}\n strokeWidth={3}\n strokeLinejoin=\"round\"\n >\n {ann.text}\n </text>\n {/* Annotation text */}\n <text\n x={textX}\n y={textY}\n fill={color}\n fontSize={fontSize}\n fontFamily=\"system-ui, sans-serif\"\n textAnchor=\"middle\"\n dominantBaseline=\"auto\"\n >\n {ann.text}\n </text>\n </g>\n );\n })}\n </g>\n );\n}\n","/**\n * Binary search utilities for snapping crosshair to nearest spectrum data.\n *\n * Given a cursor x-position, finds the closest data point on visible spectra\n * using binary search for O(log n) performance.\n */\n\nimport type { Spectrum } from \"../types\";\n\n/** Result of a snap-to-spectrum search. */\nexport interface SnapResult {\n /** Spectrum ID of the closest match. */\n spectrumId: string;\n /** Index within the spectrum's data arrays. */\n index: number;\n /** Data-space x value of the snapped point. */\n x: number;\n /** Data-space y value of the snapped point. */\n y: number;\n /** Pixel distance from cursor to the snapped point. */\n distance: number;\n}\n\n/**\n * Binary search for the index of the closest x-value in a sorted array.\n *\n * Works with both ascending and descending arrays.\n * Returns the index of the element closest to `target`.\n */\nexport function binarySearchClosest(\n arr: Float64Array | number[],\n target: number,\n length: number,\n): number {\n if (length === 0) return -1;\n if (length === 1) return 0;\n\n // Determine sort direction\n const ascending = (arr[length - 1] as number) >= (arr[0] as number);\n\n let lo = 0;\n let hi = length - 1;\n\n while (lo < hi - 1) {\n const mid = (lo + hi) >>> 1;\n const midVal = arr[mid] as number;\n\n if (ascending) {\n if (midVal <= target) lo = mid;\n else hi = mid;\n } else {\n if (midVal >= target) lo = mid;\n else hi = mid;\n }\n }\n\n // Compare lo and hi to find closest\n const dLo = Math.abs((arr[lo] as number) - target);\n const dHi = Math.abs((arr[hi] as number) - target);\n return dLo <= dHi ? lo : hi;\n}\n\n/**\n * Find the nearest data point across all visible spectra to a given\n * data-space x position. Uses pixel-space distance for ranking.\n */\nexport function snapToNearestSpectrum(\n spectra: Spectrum[],\n dataX: number,\n cursorPy: number,\n xScale: (v: number) => number,\n yScale: (v: number) => number,\n): SnapResult | null {\n let best: SnapResult | null = null;\n\n for (const spectrum of spectra) {\n if (spectrum.visible === false) continue;\n\n const n = Math.min(spectrum.x.length, spectrum.y.length);\n if (n < 2) continue;\n\n const idx = binarySearchClosest(spectrum.x, dataX, n);\n if (idx < 0) continue;\n\n const sx = spectrum.x[idx] as number;\n const sy = spectrum.y[idx] as number;\n\n // Compute pixel distance (primarily y-axis since x is at cursor)\n const pxDist = Math.abs(xScale(sx) - xScale(dataX));\n const pyDist = Math.abs(yScale(sy) - cursorPy);\n const distance = Math.sqrt(pxDist * pxDist + pyDist * pyDist);\n\n if (!best || distance < best.distance) {\n best = {\n spectrumId: spectrum.id,\n index: idx,\n x: sx,\n y: sy,\n distance,\n };\n }\n }\n\n return best;\n}\n","/**\n * Toolbar component with zoom controls and action buttons.\n *\n * Provides zoom in, zoom out, reset, and export controls\n * for the SpectraView component.\n */\n\nimport { memo } from \"react\";\nimport type { Theme } from \"../../types\";\n\nexport interface ToolbarProps {\n /** Zoom in handler. */\n onZoomIn: () => void;\n /** Zoom out handler. */\n onZoomOut: () => void;\n /** Reset zoom handler. */\n onReset: () => void;\n /** Whether the view is currently zoomed. */\n isZoomed: boolean;\n /** Theme. */\n theme: Theme;\n}\n\n/** Inline styles to avoid CSS dependency for the toolbar. */\nconst buttonStyle = (theme: Theme): React.CSSProperties => ({\n display: \"inline-flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: 28,\n height: 28,\n border: `1px solid ${theme === \"dark\" ? \"#4b5563\" : \"#d1d5db\"}`,\n borderRadius: 4,\n background: theme === \"dark\" ? \"#1f2937\" : \"#ffffff\",\n color: theme === \"dark\" ? \"#d1d5db\" : \"#374151\",\n fontSize: 14,\n cursor: \"pointer\",\n padding: 0,\n lineHeight: 1,\n});\n\nconst toolbarStyle = (theme: Theme): React.CSSProperties => ({\n display: \"flex\",\n gap: 4,\n padding: \"4px 0\",\n borderBottom: `1px solid ${theme === \"dark\" ? \"#374151\" : \"#e5e7eb\"}`,\n});\n\nexport const Toolbar = memo(function Toolbar({\n onZoomIn,\n onZoomOut,\n onReset,\n isZoomed,\n theme,\n}: ToolbarProps) {\n return (\n <div style={toolbarStyle(theme)} className=\"spectraview-toolbar\">\n <button\n type=\"button\"\n style={buttonStyle(theme)}\n onClick={onZoomIn}\n title=\"Zoom in\"\n aria-label=\"Zoom in\"\n >\n +\n </button>\n <button\n type=\"button\"\n style={buttonStyle(theme)}\n onClick={onZoomOut}\n title=\"Zoom out\"\n aria-label=\"Zoom out\"\n >\n −\n </button>\n <button\n type=\"button\"\n style={{\n ...buttonStyle(theme),\n opacity: isZoomed ? 1 : 0.4,\n }}\n onClick={onReset}\n disabled={!isZoomed}\n title=\"Reset zoom\"\n aria-label=\"Reset zoom\"\n >\n ↺\n </button>\n </div>\n );\n});\n","/**\n * Legend component showing spectrum names, color swatches, and visibility toggles.\n *\n * Supports hover highlighting and click-to-toggle visibility.\n */\n\nimport { memo } from \"react\";\nimport type { Spectrum, Theme } from \"../../types\";\nimport { getSpectrumColor } from \"../../utils/colors\";\n\n/** Position for the legend relative to the chart. */\nexport type LegendPosition = \"top\" | \"bottom\" | \"left\" | \"right\";\n\nexport interface LegendProps {\n /** Spectra to list in the legend. */\n spectra: Spectrum[];\n /** Theme for styling. */\n theme: Theme;\n /** Legend position. */\n position: LegendPosition;\n /** Callback when a spectrum's visibility is toggled. */\n onToggleVisibility?: (id: string) => void;\n /** Callback when hovering a spectrum in the legend. */\n onHighlight?: (id: string | null) => void;\n /** Currently highlighted spectrum ID. */\n highlightedId?: string | null;\n}\n\nconst containerStyle = (\n theme: Theme,\n position: LegendPosition,\n): React.CSSProperties => ({\n display: \"flex\",\n flexDirection: position === \"left\" || position === \"right\" ? \"column\" : \"row\",\n flexWrap: \"wrap\",\n gap: 6,\n padding: \"4px 8px\",\n fontSize: 12,\n fontFamily: \"system-ui, sans-serif\",\n borderTop:\n position === \"bottom\"\n ? `1px solid ${theme === \"dark\" ? \"#374151\" : \"#e5e7eb\"}`\n : undefined,\n borderBottom:\n position === \"top\"\n ? `1px solid ${theme === \"dark\" ? \"#374151\" : \"#e5e7eb\"}`\n : undefined,\n borderLeft:\n position === \"right\"\n ? `1px solid ${theme === \"dark\" ? \"#374151\" : \"#e5e7eb\"}`\n : undefined,\n borderRight:\n position === \"left\"\n ? `1px solid ${theme === \"dark\" ? \"#374151\" : \"#e5e7eb\"}`\n : undefined,\n});\n\nconst itemStyle = (\n theme: Theme,\n isHidden: boolean,\n isHighlighted: boolean,\n): React.CSSProperties => ({\n display: \"inline-flex\",\n alignItems: \"center\",\n gap: 4,\n cursor: \"pointer\",\n opacity: isHidden ? 0.4 : 1,\n fontWeight: isHighlighted ? 600 : 400,\n color: theme === \"dark\" ? \"#e5e7eb\" : \"#374151\",\n userSelect: \"none\",\n padding: \"2px 4px\",\n borderRadius: 3,\n background: isHighlighted\n ? theme === \"dark\"\n ? \"rgba(255,255,255,0.08)\"\n : \"rgba(0,0,0,0.04)\"\n : \"transparent\",\n transition: \"background 0.15s, opacity 0.15s\",\n});\n\nconst swatchStyle = (\n color: string,\n isHidden: boolean,\n): React.CSSProperties => ({\n width: 12,\n height: 3,\n borderRadius: 1,\n background: color,\n opacity: isHidden ? 0.4 : 1,\n flexShrink: 0,\n});\n\nexport const Legend = memo(function Legend({\n spectra,\n theme,\n position,\n onToggleVisibility,\n onHighlight,\n highlightedId,\n}: LegendProps) {\n if (spectra.length <= 1) return null;\n\n return (\n <div\n className=\"spectraview-legend\"\n style={containerStyle(theme, position)}\n role=\"list\"\n aria-label=\"Spectrum legend\"\n >\n {spectra.map((s, i) => {\n const color = s.color ?? getSpectrumColor(i);\n const isHidden = s.visible === false;\n const isHighlighted = highlightedId === s.id;\n\n return (\n <div\n key={s.id}\n role=\"listitem\"\n style={itemStyle(theme, isHidden, isHighlighted)}\n onClick={() => onToggleVisibility?.(s.id)}\n onMouseEnter={() => onHighlight?.(s.id)}\n onMouseLeave={() => onHighlight?.(null)}\n title={isHidden ? `Show ${s.label}` : `Hide ${s.label}`}\n >\n <span style={swatchStyle(color, isHidden)} />\n <span\n style={{\n textDecoration: isHidden ? \"line-through\" : \"none\",\n maxWidth: 120,\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n whiteSpace: \"nowrap\",\n }}\n >\n {s.label}\n </span>\n </div>\n );\n })}\n </div>\n );\n});\n","/**\n * DropZone component for drag-and-drop file loading.\n *\n * Wraps children with drag event handling and shows a visual\n * overlay when files are dragged over.\n */\n\nimport { useCallback, useState, type ReactNode } from \"react\";\nimport type { Theme } from \"../../types\";\n\nexport interface DropZoneProps {\n /** Whether drag-drop is enabled. */\n enabled: boolean;\n /** Theme for styling the overlay. */\n theme: Theme;\n /** Width of the drop zone. */\n width: number;\n /** Height of the drop zone. */\n height: number;\n /** Callback when files are dropped. */\n onDrop?: (files: File[]) => void;\n /** Children to render inside the drop zone. */\n children: ReactNode;\n}\n\nexport function DropZone({\n enabled,\n theme,\n width,\n height,\n onDrop,\n children,\n}: DropZoneProps) {\n const [isDragging, setIsDragging] = useState(false);\n const dragCountRef = { current: 0 };\n\n const handleDragEnter = useCallback(\n (e: React.DragEvent) => {\n if (!enabled) return;\n e.preventDefault();\n dragCountRef.current++;\n setIsDragging(true);\n },\n [enabled],\n );\n\n const handleDragLeave = useCallback(\n (e: React.DragEvent) => {\n if (!enabled) return;\n e.preventDefault();\n dragCountRef.current--;\n if (dragCountRef.current <= 0) {\n dragCountRef.current = 0;\n setIsDragging(false);\n }\n },\n [enabled],\n );\n\n const handleDragOver = useCallback(\n (e: React.DragEvent) => {\n if (!enabled) return;\n e.preventDefault();\n e.dataTransfer.dropEffect = \"copy\";\n },\n [enabled],\n );\n\n const handleDrop = useCallback(\n (e: React.DragEvent) => {\n if (!enabled) return;\n e.preventDefault();\n dragCountRef.current = 0;\n setIsDragging(false);\n const files = Array.from(e.dataTransfer.files);\n if (files.length > 0) {\n onDrop?.(files);\n }\n },\n [enabled, onDrop],\n );\n\n return (\n <div\n style={{ position: \"relative\", width, height }}\n onDragEnter={handleDragEnter}\n onDragLeave={handleDragLeave}\n onDragOver={handleDragOver}\n onDrop={handleDrop}\n >\n {children}\n {isDragging && (\n <div\n data-testid=\"dropzone-overlay\"\n style={{\n position: \"absolute\",\n inset: 0,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n background:\n theme === \"dark\"\n ? \"rgba(30, 58, 138, 0.6)\"\n : \"rgba(59, 130, 246, 0.15)\",\n border: `2px dashed ${theme === \"dark\" ? \"#60a5fa\" : \"#3b82f6\"}`,\n borderRadius: 4,\n zIndex: 100,\n pointerEvents: \"none\",\n fontSize: 14,\n fontFamily: \"system-ui, sans-serif\",\n color: theme === \"dark\" ? \"#93c5fd\" : \"#1d4ed8\",\n fontWeight: 500,\n }}\n >\n Drop spectrum files here\n </div>\n )}\n </div>\n );\n}\n","/**\n * Stacked display mode for comparing multiple spectra vertically.\n *\n * Each spectrum gets its own y-axis panel. Shared x-axis at the bottom.\n * Zoom/pan is synchronized across all panels.\n */\n\nimport { useMemo } from \"react\";\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { Spectrum, Margin, Theme } from \"../../types\";\nimport { computeYExtent, createYScale } from \"../../utils/scales\";\nimport { getSpectrumColor, getThemeColors } from \"../../utils/colors\";\nimport { AxisLayer } from \"../AxisLayer/AxisLayer\";\nimport { SpectrumCanvas } from \"../SpectrumCanvas/SpectrumCanvas\";\n\nexport interface StackedViewProps {\n /** Spectra to display. */\n spectra: Spectrum[];\n /** Zoomed x-scale (shared across panels). */\n xScale: ScaleLinear<number, number>;\n /** Full plot width. */\n plotWidth: number;\n /** Full plot height (will be divided among panels). */\n plotHeight: number;\n /** Chart margins. */\n margin: Margin;\n /** Theme. */\n theme: Theme;\n /** Show grid lines. */\n showGrid: boolean;\n /** X-axis label. */\n xLabel: string;\n /** Y-axis label. */\n yLabel: string;\n}\n\n/** Gap between stacked panels in pixels. */\nconst PANEL_GAP = 8;\n\nexport function StackedView({\n spectra,\n xScale,\n plotWidth,\n plotHeight,\n margin,\n theme,\n showGrid,\n xLabel,\n yLabel,\n}: StackedViewProps) {\n const visible = spectra.filter((s) => s.visible !== false);\n const colors = useMemo(() => getThemeColors(theme), [theme]);\n const panelCount = visible.length;\n const totalGap = (panelCount - 1) * PANEL_GAP;\n const panelHeight = Math.max(\n 40,\n Math.floor((plotHeight - totalGap) / Math.max(panelCount, 1)),\n );\n\n return (\n <g className=\"spectraview-stacked\">\n {visible.map((spectrum, i) => {\n const yOffset = i * (panelHeight + PANEL_GAP);\n const yExtent = computeYExtent([spectrum]);\n const yScale = createYScale(\n yExtent,\n panelHeight + margin.top + margin.bottom,\n { ...margin, top: 0, bottom: 0 },\n );\n const color = spectrum.color ?? getSpectrumColor(i);\n const coloredSpectrum = { ...spectrum, color };\n\n return (\n <g key={spectrum.id} transform={`translate(0, ${yOffset})`}>\n {/* Panel background */}\n <rect\n x={0}\n y={0}\n width={plotWidth}\n height={panelHeight}\n fill=\"transparent\"\n stroke={colors.gridColor}\n strokeWidth={0.5}\n rx={2}\n />\n\n {/* Axes */}\n <AxisLayer\n xScale={xScale}\n yScale={yScale}\n width={plotWidth}\n height={panelHeight}\n xLabel={i === panelCount - 1 ? xLabel : \"\"}\n yLabel={yLabel}\n showGrid={showGrid}\n colors={colors}\n />\n\n {/* Spectrum label */}\n <text\n x={4}\n y={14}\n fill={color}\n fontSize={11}\n fontFamily=\"system-ui, sans-serif\"\n fontWeight={500}\n >\n {spectrum.label}\n </text>\n\n {/* Canvas for this panel */}\n <foreignObject x={0} y={0} width={plotWidth} height={panelHeight}>\n <SpectrumCanvas\n spectra={[coloredSpectrum]}\n xScale={xScale}\n yScale={yScale}\n width={plotWidth}\n height={panelHeight}\n />\n </foreignObject>\n </g>\n );\n })}\n </g>\n );\n}\n","/**\n * Hook for interactive region selection via Shift+drag.\n *\n * Tracks mouse drag state when Shift is held, converting pixel\n * coordinates to data-space values using the provided x-scale.\n */\n\nimport { useCallback, useRef, useState } from \"react\";\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { Region } from \"../types\";\n\nexport interface UseRegionSelectOptions {\n /** Whether region selection is enabled. */\n enabled: boolean;\n /** X-axis scale for pixel-to-data conversion. */\n xScale: ScaleLinear<number, number>;\n /** Callback when a region is created. */\n onRegionSelect?: (region: Region) => void;\n}\n\nexport interface UseRegionSelectReturn {\n /** Pending region being dragged (null when not dragging). */\n pendingRegion: Region | null;\n /** Mouse down handler — call on the interaction rect. */\n handleMouseDown: (event: React.MouseEvent<SVGRectElement>) => void;\n /** Mouse move handler — call on the interaction rect. */\n handleMouseMove: (event: React.MouseEvent<SVGRectElement>) => void;\n /** Mouse up handler — call on the interaction rect. */\n handleMouseUp: () => void;\n}\n\nexport function useRegionSelect(\n options: UseRegionSelectOptions,\n): UseRegionSelectReturn {\n const { enabled, xScale, onRegionSelect } = options;\n const [pendingRegion, setPendingRegion] = useState<Region | null>(null);\n const dragStartRef = useRef<number | null>(null);\n\n const handleMouseDown = useCallback(\n (event: React.MouseEvent<SVGRectElement>) => {\n if (!enabled || !event.shiftKey) return;\n event.preventDefault();\n const rect = event.currentTarget.getBoundingClientRect();\n const px = event.clientX - rect.left;\n const dataX = xScale.invert(px);\n dragStartRef.current = dataX;\n setPendingRegion({ xStart: dataX, xEnd: dataX });\n },\n [enabled, xScale],\n );\n\n const handleMouseMove = useCallback(\n (event: React.MouseEvent<SVGRectElement>) => {\n if (dragStartRef.current === null) return;\n const rect = event.currentTarget.getBoundingClientRect();\n const px = event.clientX - rect.left;\n const dataX = xScale.invert(px);\n const start = dragStartRef.current;\n setPendingRegion({\n xStart: Math.min(start, dataX),\n xEnd: Math.max(start, dataX),\n });\n },\n [xScale],\n );\n\n const handleMouseUp = useCallback(() => {\n if (dragStartRef.current === null || !pendingRegion) return;\n const width = Math.abs(pendingRegion.xEnd - pendingRegion.xStart);\n if (width > 0) {\n onRegionSelect?.(pendingRegion);\n }\n dragStartRef.current = null;\n setPendingRegion(null);\n }, [pendingRegion, onRegionSelect]);\n\n return { pendingRegion, handleMouseDown, handleMouseMove, handleMouseUp };\n}\n","/**\n * Hook for responsive sizing via ResizeObserver.\n *\n * Tracks the width (and optionally height) of a container element,\n * enabling the chart to auto-size to its parent.\n */\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nexport interface ResizeObserverSize {\n width: number;\n height: number;\n}\n\nexport function useResizeObserver(): {\n ref: React.RefCallback<HTMLElement>;\n size: ResizeObserverSize | null;\n} {\n const [size, setSize] = useState<ResizeObserverSize | null>(null);\n const observerRef = useRef<ResizeObserver | null>(null);\n const elementRef = useRef<HTMLElement | null>(null);\n\n const ref = useCallback((node: HTMLElement | null) => {\n // Cleanup previous observer\n if (observerRef.current) {\n observerRef.current.disconnect();\n observerRef.current = null;\n }\n\n elementRef.current = node;\n\n if (!node) return;\n\n const observer = new ResizeObserver((entries) => {\n const entry = entries[0];\n if (!entry) return;\n const { width, height } = entry.contentRect;\n setSize({ width: Math.round(width), height: Math.round(height) });\n });\n\n observer.observe(node);\n observerRef.current = observer;\n\n // Set initial size\n const { width, height } = node.getBoundingClientRect();\n setSize({ width: Math.round(width), height: Math.round(height) });\n }, []);\n\n useEffect(() => {\n return () => {\n observerRef.current?.disconnect();\n };\n }, []);\n\n return { ref, size };\n}\n","/**\n * Hook for keyboard navigation within the spectrum viewer.\n *\n * Handles arrow keys for panning, +/- for zoom, Escape for reset.\n */\n\nimport { useCallback } from \"react\";\n\nexport interface UseKeyboardNavigationOptions {\n /** Zoom in handler. */\n onZoomIn: () => void;\n /** Zoom out handler. */\n onZoomOut: () => void;\n /** Reset zoom handler. */\n onReset: () => void;\n /** Whether keyboard navigation is enabled. */\n enabled?: boolean;\n}\n\nexport function useKeyboardNavigation(\n options: UseKeyboardNavigationOptions,\n): (event: React.KeyboardEvent) => void {\n const { onZoomIn, onZoomOut, onReset, enabled = true } = options;\n\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent) => {\n if (!enabled) return;\n\n switch (event.key) {\n case \"+\":\n case \"=\":\n event.preventDefault();\n onZoomIn();\n break;\n case \"-\":\n event.preventDefault();\n onZoomOut();\n break;\n case \"Escape\":\n event.preventDefault();\n onReset();\n break;\n }\n },\n [enabled, onZoomIn, onZoomOut, onReset],\n );\n\n return handleKeyDown;\n}\n","/**\n * Accessibility utilities for SpectraView.\n *\n * Provides helpers for reduced motion detection, ARIA label generation,\n * and keyboard navigation constants.\n */\n\n/** Check if the user prefers reduced motion. */\nexport function prefersReducedMotion(): boolean {\n if (typeof window === \"undefined\") return false;\n return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n}\n\n/** Generate an accessible description for a spectrum chart. */\nexport function generateChartDescription(\n spectrumCount: number,\n xLabel: string,\n yLabel: string,\n): string {\n if (spectrumCount === 0) return \"Empty spectrum viewer\";\n const plural = spectrumCount === 1 ? \"spectrum\" : \"spectra\";\n return `Interactive spectrum viewer showing ${spectrumCount} ${plural}. X-axis: ${xLabel}. Y-axis: ${yLabel}. Use arrow keys to pan, +/- to zoom, Escape to reset.`;\n}\n\n/** Keyboard shortcut definitions. */\nexport const KEYBOARD_SHORTCUTS = {\n PAN_LEFT: \"ArrowLeft\",\n PAN_RIGHT: \"ArrowRight\",\n PAN_UP: \"ArrowUp\",\n PAN_DOWN: \"ArrowDown\",\n ZOOM_IN: \"+\",\n ZOOM_IN_ALT: \"=\",\n ZOOM_OUT: \"-\",\n RESET: \"Escape\",\n NEXT_PEAK: \"Tab\",\n PREV_PEAK: \"Shift+Tab\",\n} as const;\n","/**\n * Minimap — A small overview component showing the full spectrum\n * with a highlighted viewport rectangle indicating the current\n * zoom region.\n *\n * Renders using Canvas for the spectrum line and SVG for the\n * viewport overlay.\n */\n\nimport { memo, useEffect, useRef, useMemo } from \"react\";\nimport { scaleLinear } from \"d3-scale\";\nimport type { Spectrum, Theme } from \"../../types\";\nimport { getThemeColors } from \"../../utils/colors\";\nimport { getSpectrumColor } from \"../../utils/colors\";\n\nexport interface MinimapProps {\n /** Spectra to render in the minimap. */\n spectra: Spectrum[];\n /** Full X extent [min, max] of the data. */\n xExtent: [number, number];\n /** Full Y extent [min, max] of the data. */\n yExtent: [number, number];\n /** Currently visible X domain from the zoomed view. */\n visibleXDomain: [number, number];\n /** Currently visible Y domain from the zoomed view (reserved for future use). */\n visibleYDomain?: [number, number];\n /** Minimap width in pixels. Defaults to 200. */\n width?: number;\n /** Minimap height in pixels. Defaults to 50. */\n height?: number;\n /** Theme. */\n theme?: Theme;\n /** Whether the view is currently zoomed. */\n isZoomed?: boolean;\n}\n\nexport const Minimap = memo(function Minimap({\n spectra,\n xExtent,\n yExtent,\n visibleXDomain,\n width = 200,\n height = 50,\n theme = \"light\",\n isZoomed = false,\n}: MinimapProps) {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const colors = useMemo(() => getThemeColors(theme), [theme]);\n\n // Full-range scales for the minimap\n const xScale = useMemo(\n () => scaleLinear().domain(xExtent).range([0, width]),\n [xExtent, width],\n );\n const yScale = useMemo(\n () => scaleLinear().domain(yExtent).range([height - 2, 2]),\n [yExtent, height],\n );\n\n // Draw spectra on canvas\n useEffect(() => {\n const ctx = canvasRef.current?.getContext(\"2d\");\n if (!ctx) return;\n\n ctx.clearRect(0, 0, width, height);\n\n for (let s = 0; s < spectra.length; s++) {\n const spectrum = spectra[s];\n if (spectrum.visible === false) continue;\n const n = Math.min(spectrum.x.length, spectrum.y.length);\n if (n < 2) continue;\n\n const color = spectrum.color ?? getSpectrumColor(s);\n ctx.beginPath();\n ctx.strokeStyle = color;\n ctx.lineWidth = 1;\n ctx.globalAlpha = 0.7;\n\n // Decimate for minimap: use every Nth point\n const step = Math.max(1, Math.floor(n / width));\n let started = false;\n\n for (let i = 0; i < n; i += step) {\n const px = xScale(spectrum.x[i] as number);\n const py = yScale(spectrum.y[i] as number);\n if (!started) {\n ctx.moveTo(px, py);\n started = true;\n } else {\n ctx.lineTo(px, py);\n }\n }\n\n ctx.stroke();\n }\n }, [spectra, xScale, yScale, width, height]);\n\n // Compute viewport rectangle position\n const vpLeft = xScale(Math.min(visibleXDomain[0], visibleXDomain[1]));\n const vpRight = xScale(Math.max(visibleXDomain[0], visibleXDomain[1]));\n const vpWidth = Math.max(vpRight - vpLeft, 2);\n\n return (\n <div\n className=\"spectraview-minimap\"\n style={{\n position: \"relative\",\n width,\n height,\n border: `1px solid ${colors.gridColor}`,\n borderRadius: 3,\n overflow: \"hidden\",\n background: colors.background,\n }}\n >\n <canvas\n ref={canvasRef}\n width={width}\n height={height}\n style={{ position: \"absolute\", top: 0, left: 0 }}\n />\n {isZoomed && (\n <svg\n width={width}\n height={height}\n style={{ position: \"absolute\", top: 0, left: 0 }}\n >\n {/* Dim outside viewport */}\n <rect\n x={0}\n y={0}\n width={vpLeft}\n height={height}\n fill={colors.background}\n opacity={0.6}\n />\n <rect\n x={vpLeft + vpWidth}\n y={0}\n width={width - vpLeft - vpWidth}\n height={height}\n fill={colors.background}\n opacity={0.6}\n />\n {/* Viewport rectangle */}\n <rect\n x={vpLeft}\n y={0}\n width={vpWidth}\n height={height}\n fill=\"none\"\n stroke={theme === \"dark\" ? \"#60a5fa\" : \"#3b82f6\"}\n strokeWidth={1.5}\n rx={1}\n />\n </svg>\n )}\n </div>\n );\n});\n","/**\n * Tooltip — Enhanced multi-spectrum data readout on hover.\n *\n * Shows values from all visible spectra at the current cursor X position,\n * with colored indicators matching each spectrum's line color.\n * Optionally shows the nearest peak annotation.\n */\n\nimport { memo, useMemo } from \"react\";\nimport type { Spectrum, Peak } from \"../../types\";\nimport type { ThemeColors } from \"../../utils/colors\";\nimport { getSpectrumColor } from \"../../utils/colors\";\nimport { binarySearchClosest } from \"../../utils/snap\";\n\nexport interface TooltipData {\n /** Cursor pixel position. */\n px: number;\n py: number;\n /** Cursor data-space position. */\n dataX: number;\n dataY: number;\n}\n\nexport interface TooltipProps {\n /** Tooltip position, or null when hidden. */\n data: TooltipData | null;\n /** Spectra for multi-value readout. */\n spectra: Spectrum[];\n /** Peaks for nearest-peak indicator. */\n peaks?: Peak[];\n /** Plot area width. */\n plotWidth: number;\n /** Plot area height. */\n plotHeight: number;\n /** Theme colors. */\n colors: ThemeColors;\n /** Number format for values. Defaults to \"auto\". */\n numberFormat?: \"auto\" | \"fixed2\" | \"fixed4\" | \"scientific\";\n}\n\nfunction formatNumber(v: number, format: string): string {\n switch (format) {\n case \"fixed2\":\n return v.toFixed(2);\n case \"fixed4\":\n return v.toFixed(4);\n case \"scientific\":\n return v.toExponential(2);\n default: // auto\n if (Math.abs(v) >= 100) return Math.round(v).toString();\n if (Math.abs(v) >= 1) return v.toFixed(2);\n if (Math.abs(v) >= 0.01) return v.toFixed(4);\n return v.toExponential(2);\n }\n}\n\nexport const Tooltip = memo(function Tooltip({\n data,\n spectra,\n peaks = [],\n plotWidth,\n plotHeight,\n colors,\n numberFormat = \"auto\",\n}: TooltipProps) {\n if (!data) return null;\n\n // Find Y values for each visible spectrum at cursor X\n const entries = useMemo(() => {\n if (!data) return [];\n return spectra\n .filter((s) => s.visible !== false)\n .map((spectrum, i) => {\n const n = Math.min(spectrum.x.length, spectrum.y.length);\n if (n < 1) return null;\n const idx = binarySearchClosest(spectrum.x, data.dataX, n);\n if (idx < 0) return null;\n return {\n label: spectrum.label,\n color: spectrum.color ?? getSpectrumColor(i),\n value: spectrum.y[idx] as number,\n x: spectrum.x[idx] as number,\n };\n })\n .filter(Boolean) as Array<{\n label: string;\n color: string;\n value: number;\n x: number;\n }>;\n }, [data?.dataX, spectra]);\n\n // Find nearest peak\n const nearestPeak = useMemo(() => {\n if (!data || peaks.length === 0) return null;\n let best: Peak | null = null;\n let bestDist = Infinity;\n for (const peak of peaks) {\n const dist = Math.abs(peak.x - data.dataX);\n if (dist < bestDist) {\n bestDist = dist;\n best = peak;\n }\n }\n return best;\n }, [data?.dataX, peaks]);\n\n const lineHeight = 16;\n const headerHeight = 18;\n const peakLineHeight = nearestPeak ? lineHeight : 0;\n const tooltipHeight = headerHeight + entries.length * lineHeight + peakLineHeight + 8;\n const tooltipWidth = 160;\n\n // Position tooltip to avoid clipping\n let tx = data.px + 15;\n let ty = data.py - tooltipHeight / 2;\n if (tx + tooltipWidth > plotWidth) tx = data.px - tooltipWidth - 15;\n if (ty < 0) ty = 4;\n if (ty + tooltipHeight > plotHeight) ty = plotHeight - tooltipHeight - 4;\n\n return (\n <g\n className=\"spectraview-tooltip\"\n transform={`translate(${tx}, ${ty})`}\n pointerEvents=\"none\"\n >\n {/* Background */}\n <rect\n x={0}\n y={0}\n width={tooltipWidth}\n height={tooltipHeight}\n rx={4}\n fill={colors.tooltipBg}\n stroke={colors.tooltipBorder}\n strokeWidth={0.5}\n opacity={0.95}\n />\n\n {/* Header: X value */}\n <text\n x={8}\n y={14}\n fill={colors.tooltipText}\n fontSize={10}\n fontFamily=\"monospace\"\n fontWeight={600}\n >\n x = {formatNumber(data.dataX, numberFormat)}\n </text>\n\n {/* Spectrum values */}\n {entries.map((entry, i) => (\n <g key={entry.label} transform={`translate(0, ${headerHeight + i * lineHeight})`}>\n <circle cx={12} cy={8} r={3} fill={entry.color} />\n <text\n x={20}\n y={11}\n fill={colors.tooltipText}\n fontSize={9}\n fontFamily=\"monospace\"\n >\n {entry.label.slice(0, 10)}: {formatNumber(entry.value, numberFormat)}\n </text>\n </g>\n ))}\n\n {/* Nearest peak */}\n {nearestPeak && (\n <text\n x={8}\n y={headerHeight + entries.length * lineHeight + 12}\n fill={colors.labelColor}\n fontSize={9}\n fontFamily=\"monospace\"\n fontStyle=\"italic\"\n >\n Peak: {nearestPeak.label ?? formatNumber(nearestPeak.x, numberFormat)}\n </text>\n )}\n </g>\n );\n});\n","/**\n * Hook for automatic peak detection in spectral data.\n *\n * Wraps the peak detection algorithm with React state management\n * and recalculates when spectra or options change.\n */\n\nimport { useMemo } from \"react\";\nimport type { Spectrum, Peak } from \"../types\";\nimport { detectPeaks, type PeakDetectionOptions } from \"../utils/peaks\";\n\nexport interface UsePeakPickingOptions extends PeakDetectionOptions {\n /** Whether peak picking is enabled. */\n enabled?: boolean;\n /** Only detect peaks for these spectrum IDs (all if not specified). */\n spectrumIds?: string[];\n}\n\n/**\n * Automatically detect peaks across one or more spectra.\n *\n * @param spectra - Array of spectra to analyze\n * @param options - Peak detection configuration\n * @returns Array of detected peaks with associated spectrum IDs\n */\nexport function usePeakPicking(\n spectra: Spectrum[],\n options: UsePeakPickingOptions = {},\n): Peak[] {\n const {\n enabled = true,\n spectrumIds,\n prominence,\n minDistance,\n maxPeaks,\n } = options;\n\n return useMemo(() => {\n if (!enabled) return [];\n\n const targetSpectra = spectrumIds\n ? spectra.filter((s) => spectrumIds.includes(s.id))\n : spectra;\n\n const allPeaks: Peak[] = [];\n\n for (const spectrum of targetSpectra) {\n if (spectrum.visible === false) continue;\n\n const peaks = detectPeaks(spectrum.x, spectrum.y, {\n prominence,\n minDistance,\n maxPeaks,\n });\n\n for (const peak of peaks) {\n allPeaks.push({\n ...peak,\n spectrumId: spectrum.id,\n });\n }\n }\n\n return allPeaks;\n }, [spectra, enabled, spectrumIds, prominence, minDistance, maxPeaks]);\n}\n","/**\n * Peak detection for spectral data.\n *\n * Uses a simple local-maxima algorithm with prominence filtering,\n * suitable for identifying peaks in IR, Raman, and NIR spectra.\n */\n\nimport type { Peak } from \"../types\";\n\n/** Default minimum prominence for peak detection. */\nconst DEFAULT_PROMINENCE = 0.01;\n\n/** Default minimum distance between peaks (in number of points). */\nconst DEFAULT_MIN_DISTANCE = 5;\n\nexport interface PeakDetectionOptions {\n /** Minimum prominence relative to the signal range. */\n prominence?: number;\n /** Minimum distance between peaks in data points. */\n minDistance?: number;\n /** Maximum number of peaks to return (sorted by prominence). */\n maxPeaks?: number;\n}\n\n/**\n * Detect peaks in a 1D signal using local maxima with prominence filtering.\n *\n * @param x - X-axis values (wavenumbers, wavelengths, etc.)\n * @param y - Y-axis values (absorbance, intensity, etc.)\n * @param options - Detection parameters\n * @returns Array of detected peaks sorted by x position\n */\nexport function detectPeaks(\n x: Float64Array | number[],\n y: Float64Array | number[],\n options: PeakDetectionOptions = {},\n): Peak[] {\n const {\n prominence = DEFAULT_PROMINENCE,\n minDistance = DEFAULT_MIN_DISTANCE,\n maxPeaks,\n } = options;\n\n if (x.length < 3 || y.length < 3) return [];\n\n // Find the signal range for prominence scaling\n let yMin = Infinity;\n let yMax = -Infinity;\n for (let i = 0; i < y.length; i++) {\n if (y[i] < yMin) yMin = y[i];\n if (y[i] > yMax) yMax = y[i];\n }\n const signalRange = yMax - yMin;\n if (signalRange === 0) return [];\n\n const absProminence = prominence * signalRange;\n\n // Find local maxima\n const candidates: { index: number; prom: number }[] = [];\n for (let i = 1; i < y.length - 1; i++) {\n if (y[i] > y[i - 1] && y[i] > y[i + 1]) {\n // Compute prominence: min drop to each side before a higher peak\n const leftMin = findMinBefore(y, i);\n const rightMin = findMinAfter(y, i);\n const prom = y[i] - Math.max(leftMin, rightMin);\n\n if (prom >= absProminence) {\n candidates.push({ index: i, prom });\n }\n }\n }\n\n // Sort by prominence (highest first) for distance filtering\n candidates.sort((a, b) => b.prom - a.prom);\n\n // Filter by minimum distance (keep most prominent first)\n const kept: { index: number; prom: number }[] = [];\n for (const c of candidates) {\n const tooClose = kept.some(\n (k) => Math.abs(k.index - c.index) < minDistance,\n );\n if (!tooClose) {\n kept.push(c);\n }\n }\n\n // Apply max peaks limit\n const selected = maxPeaks ? kept.slice(0, maxPeaks) : kept;\n\n // Convert to Peak objects, sorted by x position\n return selected\n .map((c) => ({\n x: x[c.index] as number,\n y: y[c.index] as number,\n label: formatWavenumber(x[c.index] as number),\n }))\n .sort((a, b) => a.x - b.x);\n}\n\n/**\n * Find the minimum value in y before index i, stopping at a higher peak.\n */\nfunction findMinBefore(y: Float64Array | number[], i: number): number {\n let min = y[i] as number;\n for (let j = i - 1; j >= 0; j--) {\n if (y[j] > y[i]) break;\n if ((y[j] as number) < min) min = y[j] as number;\n }\n return min;\n}\n\n/**\n * Find the minimum value in y after index i, stopping at a higher peak.\n */\nfunction findMinAfter(y: Float64Array | number[], i: number): number {\n let min = y[i] as number;\n for (let j = i + 1; j < y.length; j++) {\n if (y[j] > y[i]) break;\n if ((y[j] as number) < min) min = y[j] as number;\n }\n return min;\n}\n\n/**\n * Format a wavenumber value for display as a peak label.\n */\nfunction formatWavenumber(value: number): string {\n return Math.round(value).toString();\n}\n","/**\n * Hook for managing spectrum data loading and state.\n *\n * Handles file loading (drag-and-drop, file input), parsing,\n * and managing the collection of loaded spectra.\n */\n\nimport { useCallback, useState } from \"react\";\nimport type { Spectrum } from \"../types\";\nimport { parseCsv } from \"../parsers/csv\";\nimport { parseJson } from \"../parsers/json\";\nimport { parseJcamp } from \"../parsers/jcamp\";\n\nexport interface UseSpectrumDataReturn {\n /** Currently loaded spectra. */\n spectra: Spectrum[];\n /** Whether a file is currently being loaded. */\n loading: boolean;\n /** Last error message, if any. */\n error: string | null;\n /** Load spectra from a File object (detects format from extension). */\n loadFile: (file: File) => Promise<void>;\n /** Load spectra from a raw text string with explicit format. */\n loadText: (text: string, format: \"jcamp\" | \"csv\" | \"json\") => Promise<void>;\n /** Add a spectrum directly. */\n addSpectrum: (spectrum: Spectrum) => void;\n /** Remove a spectrum by ID. */\n removeSpectrum: (id: string) => void;\n /** Toggle a spectrum's visibility. */\n toggleVisibility: (id: string) => void;\n /** Clear all loaded spectra. */\n clear: () => void;\n}\n\n/**\n * Detect file format from file extension.\n */\nfunction detectFormat(filename: string): \"jcamp\" | \"csv\" | \"json\" | null {\n const ext = filename.toLowerCase().split(\".\").pop();\n switch (ext) {\n case \"dx\":\n case \"jdx\":\n case \"jcamp\":\n return \"jcamp\";\n case \"csv\":\n case \"tsv\":\n case \"txt\":\n return \"csv\";\n case \"json\":\n return \"json\";\n default:\n return null;\n }\n}\n\n/**\n * Hook for loading and managing spectral data.\n */\nexport function useSpectrumData(\n initialSpectra: Spectrum[] = [],\n): UseSpectrumDataReturn {\n const [spectra, setSpectra] = useState<Spectrum[]>(initialSpectra);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const loadText = useCallback(\n async (text: string, format: \"jcamp\" | \"csv\" | \"json\") => {\n setLoading(true);\n setError(null);\n\n try {\n let parsed: Spectrum[];\n\n switch (format) {\n case \"jcamp\":\n parsed = await parseJcamp(text);\n break;\n case \"csv\":\n parsed = [parseCsv(text)];\n break;\n case \"json\":\n parsed = parseJson(text);\n break;\n }\n\n setSpectra((prev) => [...prev, ...parsed]);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Failed to parse file\";\n setError(message);\n } finally {\n setLoading(false);\n }\n },\n [],\n );\n\n const loadFile = useCallback(\n async (file: File) => {\n const format = detectFormat(file.name);\n if (!format) {\n setError(`Unsupported file format: ${file.name}`);\n return;\n }\n\n const text = await file.text();\n await loadText(text, format);\n },\n [loadText],\n );\n\n const addSpectrum = useCallback((spectrum: Spectrum) => {\n setSpectra((prev) => [...prev, spectrum]);\n }, []);\n\n const removeSpectrum = useCallback((id: string) => {\n setSpectra((prev) => prev.filter((s) => s.id !== id));\n }, []);\n\n const toggleVisibility = useCallback((id: string) => {\n setSpectra((prev) =>\n prev.map((s) =>\n s.id === id ? { ...s, visible: s.visible === false ? true : false } : s,\n ),\n );\n }, []);\n\n const clear = useCallback(() => {\n setSpectra([]);\n setError(null);\n }, []);\n\n return {\n spectra,\n loading,\n error,\n loadFile,\n loadText,\n addSpectrum,\n removeSpectrum,\n toggleVisibility,\n clear,\n };\n}\n","/**\n * CSV/TSV parser for spectral data.\n *\n * Handles comma, tab, and semicolon delimiters with automatic detection.\n * Supports files with or without header rows.\n */\n\nimport type { Spectrum } from \"../types\";\n\n/** Auto-incrementing ID counter for unique spectrum IDs. */\nlet idCounter = 0;\n\nexport interface CsvParseOptions {\n /** Column delimiter (auto-detected if not provided). */\n delimiter?: string;\n /** Zero-based index of the x-value column. */\n xColumn?: number;\n /** Zero-based index of the y-value column. */\n yColumn?: number;\n /** Whether the first row is a header. */\n hasHeader?: boolean;\n /** Label for the parsed spectrum. */\n label?: string;\n}\n\n/** Delimiters to try during auto-detection. */\nconst DELIMITER_CANDIDATES = [\"\\t\", \",\", \";\", \" \"] as const;\n\n/**\n * Auto-detect the delimiter used in a CSV text.\n *\n * Counts occurrences of each candidate delimiter in the first 5 lines\n * and picks the one that appears most consistently.\n */\nfunction detectDelimiter(text: string): string {\n const lines = text.trim().split(/\\r?\\n/).slice(0, 5);\n let bestDelimiter = \",\";\n let bestScore = 0;\n\n for (const d of DELIMITER_CANDIDATES) {\n const counts = lines.map((line) => line.split(d).length - 1);\n const minCount = Math.min(...counts);\n // A good delimiter appears consistently across all lines\n if (minCount > 0 && minCount >= bestScore) {\n const consistent = counts.every((c) => c === counts[0]);\n if (consistent || minCount > bestScore) {\n bestScore = minCount;\n bestDelimiter = d;\n }\n }\n }\n\n return bestDelimiter;\n}\n\n/**\n * Parse a CSV/TSV string into a Spectrum object.\n *\n * @param text - Raw CSV/TSV text content\n * @param options - Parsing configuration\n * @returns Parsed Spectrum\n * @throws Error if the data cannot be parsed\n */\nexport function parseCsv(text: string, options: CsvParseOptions = {}): Spectrum {\n const {\n xColumn = 0,\n yColumn = 1,\n hasHeader = true,\n label = \"CSV Spectrum\",\n } = options;\n\n const delimiter = options.delimiter ?? detectDelimiter(text);\n const lines = text.trim().split(/\\r?\\n/);\n\n if (lines.length < 2) {\n throw new Error(\"CSV file must contain at least 2 lines\");\n }\n\n let headerLabel = label;\n let startIndex = 0;\n\n if (hasHeader) {\n const headers = lines[0].split(delimiter).map((h) => h.trim());\n // Only use header as label if no explicit label was provided\n if (!options.label && headers[yColumn]) {\n headerLabel = headers[yColumn];\n }\n startIndex = 1;\n }\n\n const xValues: number[] = [];\n const yValues: number[] = [];\n\n for (let i = startIndex; i < lines.length; i++) {\n const line = lines[i].trim();\n if (line === \"\" || line.startsWith(\"#\")) continue;\n\n const parts = line.split(delimiter);\n const xVal = parseFloat(parts[xColumn]);\n const yVal = parseFloat(parts[yColumn]);\n\n if (!isNaN(xVal) && !isNaN(yVal)) {\n xValues.push(xVal);\n yValues.push(yVal);\n }\n }\n\n if (xValues.length === 0) {\n throw new Error(\"No valid numeric data found in CSV\");\n }\n\n return {\n id: `csv-${++idCounter}`,\n label: headerLabel,\n x: new Float64Array(xValues),\n y: new Float64Array(yValues),\n };\n}\n\n/**\n * Parse a CSV string containing multiple y-columns into multiple spectra.\n *\n * The first column is treated as x-values, and each subsequent column\n * becomes a separate spectrum.\n */\nexport function parseCsvMulti(\n text: string,\n options: Omit<CsvParseOptions, \"xColumn\" | \"yColumn\"> = {},\n): Spectrum[] {\n const { hasHeader = true, label } = options;\n const delimiter = options.delimiter ?? detectDelimiter(text);\n const lines = text.trim().split(/\\r?\\n/);\n\n if (lines.length < 2) {\n throw new Error(\"CSV file must contain at least 2 lines\");\n }\n\n const firstDataLine = lines[hasHeader ? 1 : 0];\n const numColumns = firstDataLine.split(delimiter).length;\n\n if (numColumns < 2) {\n throw new Error(\"CSV must have at least 2 columns (x + y)\");\n }\n\n let headers: string[] | undefined;\n let startIndex = 0;\n\n if (hasHeader) {\n headers = lines[0].split(delimiter).map((h) => h.trim());\n startIndex = 1;\n }\n\n const xValues: number[] = [];\n const yArrays: number[][] = Array.from({ length: numColumns - 1 }, () => []);\n\n for (let i = startIndex; i < lines.length; i++) {\n const line = lines[i].trim();\n if (line === \"\" || line.startsWith(\"#\")) continue;\n\n const parts = line.split(delimiter);\n const xVal = parseFloat(parts[0]);\n if (isNaN(xVal)) continue;\n\n xValues.push(xVal);\n for (let col = 1; col < numColumns; col++) {\n const yVal = parseFloat(parts[col]);\n yArrays[col - 1].push(isNaN(yVal) ? 0 : yVal);\n }\n }\n\n const xArray = new Float64Array(xValues);\n\n return yArrays.map((yArr, i) => ({\n id: `csv-${++idCounter}`,\n label: label ?? headers?.[i + 1] ?? `Spectrum ${i + 1}`,\n x: xArray,\n y: new Float64Array(yArr),\n }));\n}\n","/**\n * JSON parser for spectral data.\n *\n * Supports multiple JSON formats commonly used for spectral data exchange.\n */\n\nimport type { Spectrum, SpectrumType } from \"../types\";\n\n/** Auto-incrementing ID counter for unique spectrum IDs. */\nlet idCounter = 0;\n\n/**\n * JSON spectrum format: object with x and y arrays.\n *\n * Accepts objects like:\n * ```json\n * {\n * \"label\": \"My Spectrum\",\n * \"x\": [4000, 3999, ...],\n * \"y\": [0.1, 0.12, ...],\n * \"xUnit\": \"cm⁻¹\",\n * \"yUnit\": \"Absorbance\"\n * }\n * ```\n *\n * Also accepts arrays of such objects for multi-spectrum data.\n */\ninterface JsonSpectrumInput {\n label?: string;\n title?: string;\n name?: string;\n x: number[];\n y: number[];\n wavenumbers?: number[];\n wavelengths?: number[];\n intensities?: number[];\n absorbance?: number[];\n xUnit?: string;\n yUnit?: string;\n type?: SpectrumType;\n meta?: Record<string, string | number>;\n}\n\n/**\n * Parse a JSON string into one or more Spectrum objects.\n *\n * Handles both single spectrum objects and arrays of spectra.\n * Supports flexible key names (x/wavenumbers, y/intensities, etc.).\n *\n * @param text - Raw JSON string\n * @returns Array of parsed Spectrum objects\n * @throws Error if the JSON cannot be parsed or has invalid structure\n */\nexport function parseJson(text: string): Spectrum[] {\n let data: unknown;\n try {\n data = JSON.parse(text);\n } catch {\n throw new Error(\"Invalid JSON: failed to parse input\");\n }\n\n if (Array.isArray(data)) {\n return data.map((item, i) => parseSingleJson(item as JsonSpectrumInput, i));\n }\n\n if (typeof data === \"object\" && data !== null) {\n // Check if it's a wrapper object with a \"spectra\" array\n const obj = data as Record<string, unknown>;\n if (Array.isArray(obj.spectra)) {\n return (obj.spectra as JsonSpectrumInput[]).map((item, i) =>\n parseSingleJson(item, i),\n );\n }\n return [parseSingleJson(data as JsonSpectrumInput, 0)];\n }\n\n throw new Error(\"Invalid JSON structure: expected an object or array\");\n}\n\n/**\n * Parse a single JSON object into a Spectrum.\n */\nfunction parseSingleJson(input: JsonSpectrumInput, index: number): Spectrum {\n // Resolve x values from various key names\n const xRaw = input.x ?? input.wavenumbers ?? input.wavelengths;\n if (!xRaw || !Array.isArray(xRaw)) {\n throw new Error(\n `Spectrum ${index}: missing x-axis data (expected \"x\", \"wavenumbers\", or \"wavelengths\")`,\n );\n }\n\n // Resolve y values from various key names\n const yRaw = input.y ?? input.intensities ?? input.absorbance;\n if (!yRaw || !Array.isArray(yRaw)) {\n throw new Error(\n `Spectrum ${index}: missing y-axis data (expected \"y\", \"intensities\", or \"absorbance\")`,\n );\n }\n\n if (xRaw.length !== yRaw.length) {\n throw new Error(\n `Spectrum ${index}: x and y arrays must have the same length (got ${xRaw.length} and ${yRaw.length})`,\n );\n }\n\n const label = input.label ?? input.title ?? input.name ?? `Spectrum ${index + 1}`;\n\n return {\n id: `json-${++idCounter}`,\n label,\n x: new Float64Array(xRaw),\n y: new Float64Array(yRaw),\n xUnit: input.xUnit,\n yUnit: input.yUnit,\n type: input.type,\n meta: input.meta,\n };\n}\n","/**\n * JCAMP-DX parser for spectral data.\n *\n * Wraps the `jcampconverter` npm package (optional peer dependency)\n * to parse .dx, .jdx, and .jcamp files into Spectrum objects.\n *\n * If jcampconverter is not installed, a lightweight built-in parser\n * handles basic AFFN (ASCII Free Format Numeric) JCAMP-DX files.\n */\n\nimport type { Spectrum, SpectrumType } from \"../types\";\n\n/** Auto-incrementing ID counter for unique spectrum IDs. */\nlet idCounter = 0;\n\n/**\n * Minimal shape of jcampconverter output (to avoid hard dependency).\n */\ninterface JcampResult {\n flatten: Array<{\n spectra: Array<{\n data: Array<{\n x: number[];\n y: number[];\n }>;\n }>;\n info: Record<string, string>;\n }>;\n}\n\n/** Cached reference to jcampconverter (lazy-loaded). */\nlet converterModule: { convert: (text: string, options?: object) => JcampResult } | null =\n null;\nlet converterChecked = false;\n\n/**\n * Attempt to dynamically import jcampconverter.\n *\n * Uses a variable to prevent bundlers from statically analyzing the import,\n * since jcampconverter is an optional peer dependency.\n */\nasync function getConverter(): Promise<typeof converterModule> {\n if (converterChecked) return converterModule;\n converterChecked = true;\n try {\n // Variable prevents Vite/Webpack static import analysis\n const pkg = \"jcampconverter\";\n converterModule = await import(/* @vite-ignore */ pkg);\n } catch {\n converterModule = null;\n }\n return converterModule;\n}\n\n/**\n * Infer spectrum type from JCAMP-DX header metadata.\n */\nfunction inferType(info: Record<string, string>): SpectrumType {\n const dataType = (info[\"DATA TYPE\"] ?? info[\"DATATYPE\"] ?? \"\").toLowerCase();\n if (dataType.includes(\"infrared\") || dataType.includes(\"ir\")) return \"IR\";\n if (dataType.includes(\"raman\")) return \"Raman\";\n if (dataType.includes(\"nir\") || dataType.includes(\"near\")) return \"NIR\";\n if (dataType.includes(\"uv\") || dataType.includes(\"vis\")) return \"UV-Vis\";\n if (dataType.includes(\"fluor\")) return \"fluorescence\";\n return \"other\";\n}\n\n/**\n * Parse a JCAMP-DX string into Spectrum objects.\n *\n * Uses jcampconverter if available, otherwise falls back to the built-in\n * parser for basic AFFN format files.\n *\n * @param text - Raw JCAMP-DX file content\n * @returns Array of parsed Spectrum objects\n */\nexport async function parseJcamp(text: string): Promise<Spectrum[]> {\n const converter = await getConverter();\n if (converter) {\n return parseWithConverter(text, converter);\n }\n return [parseBasicJcamp(text)];\n}\n\n/**\n * Parse using jcampconverter library.\n */\nfunction parseWithConverter(\n text: string,\n converter: NonNullable<typeof converterModule>,\n): Spectrum[] {\n const result = converter.convert(text, { keepRecordsRegExp: /.*/ });\n\n return result.flatten.map((entry, i) => {\n const firstSpectrum = entry.spectra?.[0]?.data?.[0];\n if (!firstSpectrum) {\n throw new Error(`JCAMP block ${i}: no spectral data found`);\n }\n\n return {\n id: `jcamp-${++idCounter}`,\n label: entry.info?.TITLE ?? `Spectrum ${i + 1}`,\n x: new Float64Array(firstSpectrum.x),\n y: new Float64Array(firstSpectrum.y),\n xUnit: entry.info?.XUNITS ?? \"cm⁻¹\",\n yUnit: entry.info?.YUNITS ?? \"Absorbance\",\n type: inferType(entry.info),\n meta: entry.info,\n };\n });\n}\n\n/**\n * Lightweight built-in JCAMP-DX parser for basic AFFN format.\n *\n * Handles the most common case: single-spectrum files with\n * XYDATA=(X++(Y..Y)) in ASCII free-format.\n *\n * This does NOT support compressed formats (SQZ, DIF, DIFDUP, NTUP)\n * or multi-block files. For full support, install jcampconverter.\n */\nfunction parseBasicJcamp(text: string): Spectrum {\n const lines = text.split(/\\r?\\n/);\n const info: Record<string, string> = {};\n const xValues: number[] = [];\n const yValues: number[] = [];\n\n let inData = false;\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Parse labeled data records (##KEY= value)\n if (trimmed.startsWith(\"##\")) {\n const match = trimmed.match(/^##(.+?)=\\s*(.*)$/);\n if (match) {\n const key = match[1].trim().toUpperCase();\n const value = match[2].trim();\n\n if (key === \"XYDATA\" || key === \"XYPOINTS\") {\n inData = true;\n continue;\n }\n if (key === \"END\") {\n inData = false;\n continue;\n }\n\n info[key] = value;\n }\n continue;\n }\n\n // Parse data lines\n if (inData && trimmed !== \"\") {\n const values = trimmed.split(/[\\s,]+/).map(Number);\n if (values.length >= 2 && !values.some(isNaN)) {\n // XYDATA: first value is X, rest are Y values\n const x0 = values[0];\n const firstX = parseFloat(info[\"FIRSTX\"] ?? \"0\");\n const lastX = parseFloat(info[\"LASTX\"] ?? \"0\");\n const npoints = parseInt(info[\"NPOINTS\"] ?? \"0\", 10);\n\n if (npoints > 0 && values.length === 2) {\n // Simple X,Y pair format\n xValues.push(values[0]);\n yValues.push(values[1]);\n } else if (values.length > 1) {\n // X++(Y..Y) format — X is first, rest are Y\n const deltaX =\n npoints > 1 ? (lastX - firstX) / (npoints - 1) : 0;\n for (let j = 1; j < values.length; j++) {\n xValues.push(x0 + (j - 1) * deltaX);\n yValues.push(values[j]);\n }\n }\n }\n }\n }\n\n if (xValues.length === 0) {\n throw new Error(\n \"Failed to parse JCAMP-DX: no data found. Install jcampconverter for full format support.\",\n );\n }\n\n return {\n id: `jcamp-${++idCounter}`,\n label: info[\"TITLE\"] ?? \"JCAMP Spectrum\",\n x: new Float64Array(xValues),\n y: new Float64Array(yValues),\n xUnit: info[\"XUNITS\"] ?? \"cm⁻¹\",\n yUnit: info[\"YUNITS\"] ?? \"Absorbance\",\n type: inferType(info),\n meta: info,\n };\n}\n","/**\n * Hook for exporting the spectrum view as PNG, SVG, or CSV data.\n */\n\nimport { useCallback } from \"react\";\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { Spectrum } from \"../types\";\nimport { generateSvg, downloadSvg } from \"../utils/svg-export\";\n\nexport interface UseExportReturn {\n /** Export the canvas as a PNG data URL. */\n exportPng: (canvas: HTMLCanvasElement, filename?: string) => void;\n /** Export visible spectra as SVG vector. */\n exportSvg: (\n spectra: Spectrum[],\n xScale: ScaleLinear<number, number>,\n yScale: ScaleLinear<number, number>,\n width: number,\n height: number,\n filename?: string,\n ) => void;\n /** Export visible spectra as CSV text. */\n exportCsv: (spectra: Spectrum[], filename?: string) => void;\n /** Export visible spectra as JSON. */\n exportJson: (spectra: Spectrum[], filename?: string) => void;\n}\n\n/**\n * Trigger a file download in the browser.\n */\nfunction downloadBlob(blob: Blob, filename: string): void {\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n}\n\n/**\n * Hook for exporting spectrum data and visualizations.\n */\nexport function useExport(): UseExportReturn {\n const exportPng = useCallback(\n (canvas: HTMLCanvasElement, filename = \"spectrum.png\") => {\n canvas.toBlob((blob) => {\n if (blob) downloadBlob(blob, filename);\n }, \"image/png\");\n },\n [],\n );\n\n const exportCsv = useCallback(\n (spectra: Spectrum[], filename = \"spectra.csv\") => {\n const visible = spectra.filter((s) => s.visible !== false);\n if (visible.length === 0) return;\n\n // Build CSV with shared x-axis or per-spectrum columns\n if (visible.length === 1) {\n const s = visible[0];\n const header = `${s.xUnit ?? \"x\"},${s.yUnit ?? \"y\"}\\n`;\n const rows = Array.from(s.x).map(\n (x, i) => `${x},${s.y[i]}`,\n );\n const csv = header + rows.join(\"\\n\");\n downloadBlob(new Blob([csv], { type: \"text/csv\" }), filename);\n } else {\n // Multi-spectrum: each gets its own x,y column pair\n const maxLen = Math.max(...visible.map((s) => s.x.length));\n const header = visible\n .map((s) => `${s.label}_x,${s.label}_y`)\n .join(\",\");\n const rows: string[] = [];\n for (let i = 0; i < maxLen; i++) {\n const values = visible.map((s) => {\n if (i < s.x.length) return `${s.x[i]},${s.y[i]}`;\n return \",\";\n });\n rows.push(values.join(\",\"));\n }\n const csv = header + \"\\n\" + rows.join(\"\\n\");\n downloadBlob(new Blob([csv], { type: \"text/csv\" }), filename);\n }\n },\n [],\n );\n\n const exportJson = useCallback(\n (spectra: Spectrum[], filename = \"spectra.json\") => {\n const visible = spectra.filter((s) => s.visible !== false);\n const output = visible.map((s) => ({\n label: s.label,\n x: Array.from(s.x),\n y: Array.from(s.y),\n xUnit: s.xUnit,\n yUnit: s.yUnit,\n type: s.type,\n }));\n const json = JSON.stringify(output, null, 2);\n downloadBlob(new Blob([json], { type: \"application/json\" }), filename);\n },\n [],\n );\n\n const exportSvg = useCallback(\n (\n spectra: Spectrum[],\n xScale: ScaleLinear<number, number>,\n yScale: ScaleLinear<number, number>,\n width: number,\n height: number,\n filename = \"spectrum.svg\",\n ) => {\n const svg = generateSvg(spectra, xScale, yScale, { width, height });\n downloadSvg(svg, filename);\n },\n [],\n );\n\n return { exportPng, exportSvg, exportCsv, exportJson };\n}\n","/**\n * SVG export utility for generating publication-quality vector figures.\n *\n * Serializes a chart's SVG element to a standalone SVG string with\n * embedded spectral data paths.\n */\n\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { Spectrum } from \"../types\";\nimport { getSpectrumColor } from \"./colors\";\n\n/** Line dash patterns for different line styles. */\nexport const LINE_DASH_PATTERNS: Record<string, string> = {\n solid: \"\",\n dashed: \"8 4\",\n dotted: \"2 2\",\n \"dash-dot\": \"8 4 2 4\",\n};\n\n/** Supported line style values. */\nexport type LineStyle = \"solid\" | \"dashed\" | \"dotted\" | \"dash-dot\";\n\nexport interface SvgExportOptions {\n /** Width of the SVG. */\n width: number;\n /** Height of the SVG. */\n height: number;\n /** Background color. */\n background?: string;\n /** Title text for the SVG. */\n title?: string;\n}\n\n/**\n * Generate an SVG string for the given spectra.\n */\nexport function generateSvg(\n spectra: Spectrum[],\n xScale: ScaleLinear<number, number>,\n yScale: ScaleLinear<number, number>,\n options: SvgExportOptions,\n): string {\n const { width, height, background = \"#ffffff\", title } = options;\n\n const paths = spectra\n .filter((s) => s.visible !== false)\n .map((s, i) => {\n const color = s.color ?? getSpectrumColor(i);\n const lineStyle = (s as SpectrumWithStyle).lineStyle ?? \"solid\";\n const lineWidth = (s as SpectrumWithStyle).lineWidth ?? 1.5;\n const dashArray = LINE_DASH_PATTERNS[lineStyle] ?? \"\";\n\n const n = Math.min(s.x.length, s.y.length);\n if (n < 2) return \"\";\n\n const points: string[] = [];\n for (let j = 0; j < n; j++) {\n const px = xScale(s.x[j] as number).toFixed(2);\n const py = yScale(s.y[j] as number).toFixed(2);\n points.push(`${j === 0 ? \"M\" : \"L\"}${px},${py}`);\n }\n\n return `<path d=\"${points.join(\" \")}\" fill=\"none\" stroke=\"${color}\" stroke-width=\"${lineWidth}\"${dashArray ? ` stroke-dasharray=\"${dashArray}\"` : \"\"}/>\\n <!-- ${s.label} -->`;\n })\n .filter(Boolean)\n .join(\"\\n \");\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\" viewBox=\"0 0 ${width} ${height}\">\n <rect width=\"${width}\" height=\"${height}\" fill=\"${background}\"/>\n ${title ? `<text x=\"${width / 2}\" y=\"20\" text-anchor=\"middle\" font-family=\"system-ui\" font-size=\"14\">${title}</text>` : \"\"}\n <g>\n ${paths}\n </g>\n</svg>`;\n}\n\n/**\n * Download an SVG string as a file.\n */\nexport function downloadSvg(svg: string, filename = \"spectrum.svg\"): void {\n const blob = new Blob([svg], { type: \"image/svg+xml\" });\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n}\n\n/** Extended Spectrum with line style properties. */\ninterface SpectrumWithStyle extends Spectrum {\n lineStyle?: LineStyle;\n lineWidth?: number;\n}\n","/**\n * Hook for applying spectral normalization/processing transformations.\n *\n * Takes raw spectra and a normalization mode, returns transformed spectra\n * ready for rendering. All transformations are memoized.\n */\n\nimport { useMemo } from \"react\";\nimport type { Spectrum } from \"../types\";\nimport {\n normalizeMinMax,\n normalizeArea,\n normalizeSNV,\n baselineRubberBand,\n smoothSavitzkyGolay,\n derivative1st,\n} from \"../utils/processing\";\n\n/** Available normalization/processing modes. */\nexport type NormalizationMode =\n | \"none\"\n | \"min-max\"\n | \"area\"\n | \"snv\"\n | \"baseline\"\n | \"smooth\"\n | \"derivative\";\n\nexport interface UseNormalizationOptions {\n /** Input spectra. */\n spectra: Spectrum[];\n /** Active normalization mode. */\n mode: NormalizationMode;\n /** Smoothing window size (for \"smooth\" mode). Defaults to 7. */\n smoothWindow?: number;\n}\n\nexport interface UseNormalizationReturn {\n /** Transformed spectra. */\n spectra: Spectrum[];\n /** Current mode label for display. */\n modeLabel: string;\n}\n\nconst MODE_LABELS: Record<NormalizationMode, string> = {\n none: \"Raw\",\n \"min-max\": \"Min-Max\",\n area: \"Area Normalized\",\n snv: \"SNV\",\n baseline: \"Baseline Corrected\",\n smooth: \"Smoothed\",\n derivative: \"1st Derivative\",\n};\n\nfunction transformSpectrum(\n spectrum: Spectrum,\n mode: NormalizationMode,\n smoothWindow: number,\n): Spectrum {\n if (mode === \"none\") return spectrum;\n\n let newY: Float64Array;\n\n switch (mode) {\n case \"min-max\":\n newY = normalizeMinMax(spectrum.y);\n break;\n case \"area\":\n newY = normalizeArea(spectrum.x, spectrum.y);\n break;\n case \"snv\":\n newY = normalizeSNV(spectrum.y);\n break;\n case \"baseline\":\n newY = baselineRubberBand(spectrum.y);\n break;\n case \"smooth\":\n newY = smoothSavitzkyGolay(spectrum.y, smoothWindow);\n break;\n case \"derivative\":\n newY = derivative1st(spectrum.x, spectrum.y);\n break;\n default:\n return spectrum;\n }\n\n return { ...spectrum, y: newY };\n}\n\nexport function useNormalization({\n spectra,\n mode,\n smoothWindow = 7,\n}: UseNormalizationOptions): UseNormalizationReturn {\n const transformed = useMemo(\n () => spectra.map((s) => transformSpectrum(s, mode, smoothWindow)),\n [spectra, mode, smoothWindow],\n );\n\n return {\n spectra: transformed,\n modeLabel: MODE_LABELS[mode],\n };\n}\n","/**\n * Spectral processing utilities.\n *\n * Pure functions for common spectral data transformations:\n * - Baseline correction (rubber-band)\n * - Normalization (min-max, area, SNV)\n * - Smoothing (Savitzky-Golay)\n * - Numerical derivatives (1st, 2nd)\n *\n * All functions return new arrays, never mutating inputs.\n *\n * @module processing\n */\n\n// ─── Baseline Correction ───────────────────────────────────────────\n\n/**\n * Rubber-band baseline correction.\n *\n * Computes the convex hull of the spectrum from below, then subtracts\n * the interpolated baseline. This is a simple, robust method for\n * removing broad fluorescence backgrounds.\n *\n * @param y - Input Y values\n * @returns Baseline-corrected Y values\n */\nexport function baselineRubberBand(y: Float64Array | number[]): Float64Array {\n const n = y.length;\n if (n < 3) return new Float64Array(y);\n\n // Find convex hull from below (lower envelope)\n const hullIndices: number[] = [0];\n for (let i = 1; i < n; i++) {\n while (hullIndices.length >= 2) {\n const j = hullIndices.length - 1;\n const a = hullIndices[j - 1];\n const b = hullIndices[j];\n // Cross product test: for lower hull, pop if b is above line a→i\n const cross =\n (i - a) * ((y[b] as number) - (y[a] as number)) -\n (b - a) * ((y[i] as number) - (y[a] as number));\n if (cross >= 0) {\n hullIndices.pop();\n } else {\n break;\n }\n }\n hullIndices.push(i);\n }\n\n // Interpolate baseline from hull\n const baseline = new Float64Array(n);\n let hi = 0;\n for (let i = 0; i < n; i++) {\n while (hi < hullIndices.length - 1 && hullIndices[hi + 1] <= i) {\n hi++;\n }\n if (hi >= hullIndices.length - 1) {\n baseline[i] = y[hullIndices[hullIndices.length - 1]] as number;\n } else {\n const a = hullIndices[hi];\n const b = hullIndices[hi + 1];\n const t = (i - a) / (b - a);\n baseline[i] =\n (y[a] as number) * (1 - t) + (y[b] as number) * t;\n }\n }\n\n // Subtract baseline\n const result = new Float64Array(n);\n for (let i = 0; i < n; i++) {\n result[i] = (y[i] as number) - baseline[i];\n }\n return result;\n}\n\n// ─── Normalization ─────────────────────────────────────────────────\n\n/**\n * Min-max normalization to [0, 1] range.\n */\nexport function normalizeMinMax(y: Float64Array | number[]): Float64Array {\n const n = y.length;\n const result = new Float64Array(n);\n let min = Infinity;\n let max = -Infinity;\n\n for (let i = 0; i < n; i++) {\n const v = y[i] as number;\n if (v < min) min = v;\n if (v > max) max = v;\n }\n\n const range = max - min;\n if (range === 0) return result; // all zeros\n\n for (let i = 0; i < n; i++) {\n result[i] = ((y[i] as number) - min) / range;\n }\n return result;\n}\n\n/**\n * Area normalization: divide by total area under the curve.\n *\n * Uses trapezoidal integration. The resulting spectrum has unit area.\n */\nexport function normalizeArea(\n x: Float64Array | number[],\n y: Float64Array | number[],\n): Float64Array {\n const n = Math.min(x.length, y.length);\n if (n < 2) return new Float64Array(y);\n\n // Trapezoidal area\n let area = 0;\n for (let i = 1; i < n; i++) {\n area +=\n Math.abs((x[i] as number) - (x[i - 1] as number)) *\n (Math.abs(y[i] as number) + Math.abs(y[i - 1] as number)) *\n 0.5;\n }\n\n if (area === 0) return new Float64Array(y);\n\n const result = new Float64Array(n);\n for (let i = 0; i < n; i++) {\n result[i] = (y[i] as number) / area;\n }\n return result;\n}\n\n/**\n * Standard Normal Variate (SNV) normalization.\n *\n * Centers the spectrum by subtracting the mean, then divides by\n * the standard deviation. Common in chemometrics.\n */\nexport function normalizeSNV(y: Float64Array | number[]): Float64Array {\n const n = y.length;\n if (n === 0) return new Float64Array(0);\n\n let sum = 0;\n for (let i = 0; i < n; i++) {\n sum += y[i] as number;\n }\n const mean = sum / n;\n\n let variance = 0;\n for (let i = 0; i < n; i++) {\n const d = (y[i] as number) - mean;\n variance += d * d;\n }\n const std = Math.sqrt(variance / n);\n\n if (std === 0) return new Float64Array(n);\n\n const result = new Float64Array(n);\n for (let i = 0; i < n; i++) {\n result[i] = ((y[i] as number) - mean) / std;\n }\n return result;\n}\n\n// ─── Smoothing ─────────────────────────────────────────────────────\n\n/**\n * Savitzky-Golay smoothing (polynomial order 2).\n *\n * Uses pre-computed convolution coefficients for common window sizes.\n * Falls back to a simple moving average for unsupported window sizes.\n *\n * @param y - Input Y values\n * @param windowSize - Must be odd and >= 3. Defaults to 5.\n * @returns Smoothed Y values\n */\nexport function smoothSavitzkyGolay(\n y: Float64Array | number[],\n windowSize = 5,\n): Float64Array {\n const n = y.length;\n if (n < windowSize || windowSize < 3) return new Float64Array(y);\n\n // Ensure odd window\n const w = windowSize % 2 === 0 ? windowSize + 1 : windowSize;\n const halfW = (w - 1) / 2;\n\n // Pre-computed SG coefficients (quadratic, normalized)\n const coefficients = getSGCoefficients(w);\n\n const result = new Float64Array(n);\n\n // Copy edges unchanged\n for (let i = 0; i < halfW; i++) {\n result[i] = y[i] as number;\n result[n - 1 - i] = y[n - 1 - i] as number;\n }\n\n // Apply convolution\n for (let i = halfW; i < n - halfW; i++) {\n let sum = 0;\n for (let j = -halfW; j <= halfW; j++) {\n sum += coefficients[j + halfW] * (y[i + j] as number);\n }\n result[i] = sum;\n }\n\n return result;\n}\n\n/**\n * Get Savitzky-Golay coefficients for quadratic polynomial smoothing.\n */\nfunction getSGCoefficients(windowSize: number): number[] {\n // Pre-computed for common sizes (quadratic smoothing)\n const precomputed: Record<number, number[]> = {\n 5: [-3, 12, 17, 12, -3].map((v) => v / 35),\n 7: [-2, 3, 6, 7, 6, 3, -2].map((v) => v / 21),\n 9: [-21, 14, 39, 54, 59, 54, 39, 14, -21].map((v) => v / 231),\n 11: [-36, 9, 44, 69, 84, 89, 84, 69, 44, 9, -36].map((v) => v / 429),\n };\n\n if (precomputed[windowSize]) {\n return precomputed[windowSize];\n }\n\n // Fallback: uniform weights (simple moving average)\n return Array(windowSize).fill(1 / windowSize);\n}\n\n// ─── Derivatives ───────────────────────────────────────────────────\n\n/**\n * First derivative using central differences.\n *\n * @param x - X values (for spacing)\n * @param y - Y values\n * @returns First derivative dy/dx\n */\nexport function derivative1st(\n x: Float64Array | number[],\n y: Float64Array | number[],\n): Float64Array {\n const n = Math.min(x.length, y.length);\n if (n < 2) return new Float64Array(n);\n\n const result = new Float64Array(n);\n\n // Forward difference for first point\n result[0] =\n ((y[1] as number) - (y[0] as number)) /\n ((x[1] as number) - (x[0] as number));\n\n // Central differences for interior\n for (let i = 1; i < n - 1; i++) {\n result[i] =\n ((y[i + 1] as number) - (y[i - 1] as number)) /\n ((x[i + 1] as number) - (x[i - 1] as number));\n }\n\n // Backward difference for last point\n result[n - 1] =\n ((y[n - 1] as number) - (y[n - 2] as number)) /\n ((x[n - 1] as number) - (x[n - 2] as number));\n\n return result;\n}\n\n/**\n * Second derivative using central differences.\n *\n * @param x - X values (for spacing)\n * @param y - Y values\n * @returns Second derivative d²y/dx²\n */\nexport function derivative2nd(\n x: Float64Array | number[],\n y: Float64Array | number[],\n): Float64Array {\n const n = Math.min(x.length, y.length);\n if (n < 3) return new Float64Array(n);\n\n const result = new Float64Array(n);\n\n // Interior points using central difference\n for (let i = 1; i < n - 1; i++) {\n const dx1 = (x[i] as number) - (x[i - 1] as number);\n const dx2 = (x[i + 1] as number) - (x[i] as number);\n const dxAvg = (dx1 + dx2) / 2;\n result[i] =\n ((y[i + 1] as number) - 2 * (y[i] as number) + (y[i - 1] as number)) /\n (dxAvg * dxAvg);\n }\n\n // Copy boundary values from nearest interior\n result[0] = result[1];\n result[n - 1] = result[n - 2];\n\n return result;\n}\n","/**\n * Generic undo/redo history hook using a command pattern.\n *\n * Stores snapshots of state in an undo/redo stack. Supports\n * arbitrary state types — works for zoom states, processing\n * configurations, or any serializable value.\n *\n * @module useHistory\n */\n\nimport { useCallback, useRef, useState } from \"react\";\n\nexport interface UseHistoryOptions<T> {\n /** Initial state. */\n initialState: T;\n /** Maximum history depth. Defaults to 50. */\n maxDepth?: number;\n}\n\nexport interface UseHistoryReturn<T> {\n /** Current state. */\n state: T;\n /** Push a new state onto the history stack. */\n push: (state: T) => void;\n /** Undo to the previous state. Returns false if nothing to undo. */\n undo: () => boolean;\n /** Redo to the next state. Returns false if nothing to redo. */\n redo: () => boolean;\n /** Reset history to initial state. */\n reset: () => void;\n /** Whether undo is available. */\n canUndo: boolean;\n /** Whether redo is available. */\n canRedo: boolean;\n /** Number of states in the undo stack. */\n undoCount: number;\n /** Number of states in the redo stack. */\n redoCount: number;\n}\n\nexport function useHistory<T>({\n initialState,\n maxDepth = 50,\n}: UseHistoryOptions<T>): UseHistoryReturn<T> {\n const [state, setState] = useState<T>(initialState);\n const undoStack = useRef<T[]>([]);\n const redoStack = useRef<T[]>([]);\n // Track stack lengths for reactivity\n const [undoCount, setUndoCount] = useState(0);\n const [redoCount, setRedoCount] = useState(0);\n\n const push = useCallback(\n (newState: T) => {\n setState((prev) => {\n undoStack.current.push(prev);\n if (undoStack.current.length > maxDepth) {\n undoStack.current.shift();\n }\n setUndoCount(undoStack.current.length);\n // Clear redo stack on new action\n redoStack.current = [];\n setRedoCount(0);\n return newState;\n });\n },\n [maxDepth],\n );\n\n const undo = useCallback(() => {\n const prev = undoStack.current.pop();\n if (prev === undefined) return false;\n setState((current) => {\n redoStack.current.push(current);\n setRedoCount(redoStack.current.length);\n setUndoCount(undoStack.current.length);\n return prev;\n });\n return true;\n }, []);\n\n const redo = useCallback(() => {\n const next = redoStack.current.pop();\n if (next === undefined) return false;\n setState((current) => {\n undoStack.current.push(current);\n setUndoCount(undoStack.current.length);\n setRedoCount(redoStack.current.length);\n return next;\n });\n return true;\n }, []);\n\n const reset = useCallback(() => {\n setState(initialState);\n undoStack.current = [];\n redoStack.current = [];\n setUndoCount(0);\n setRedoCount(0);\n }, [initialState]);\n\n return {\n state,\n push,\n undo,\n redo,\n reset,\n canUndo: undoCount > 0,\n canRedo: redoCount > 0,\n undoCount,\n redoCount,\n };\n}\n","/**\n * Export dropdown menu for saving spectra in various formats.\n */\n\nimport { useCallback, useState } from \"react\";\nimport type { Theme } from \"../../types\";\n\nexport interface ExportMenuProps {\n /** Theme for styling. */\n theme: Theme;\n /** Export as PNG. */\n onExportPng?: () => void;\n /** Export as SVG. */\n onExportSvg?: () => void;\n /** Export as CSV. */\n onExportCsv?: () => void;\n /** Export as JSON. */\n onExportJson?: () => void;\n}\n\nconst menuButtonStyle = (theme: Theme): React.CSSProperties => ({\n display: \"inline-flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n height: 28,\n padding: \"0 8px\",\n border: `1px solid ${theme === \"dark\" ? \"#4b5563\" : \"#d1d5db\"}`,\n borderRadius: 4,\n background: theme === \"dark\" ? \"#1f2937\" : \"#ffffff\",\n color: theme === \"dark\" ? \"#d1d5db\" : \"#374151\",\n fontSize: 12,\n cursor: \"pointer\",\n lineHeight: 1,\n position: \"relative\" as const,\n});\n\nconst dropdownStyle = (theme: Theme): React.CSSProperties => ({\n position: \"absolute\" as const,\n top: 30,\n left: 0,\n background: theme === \"dark\" ? \"#1f2937\" : \"#ffffff\",\n border: `1px solid ${theme === \"dark\" ? \"#4b5563\" : \"#d1d5db\"}`,\n borderRadius: 4,\n boxShadow: \"0 2px 8px rgba(0,0,0,0.15)\",\n zIndex: 200,\n minWidth: 100,\n overflow: \"hidden\",\n});\n\nconst optionStyle = (theme: Theme): React.CSSProperties => ({\n display: \"block\",\n width: \"100%\",\n padding: \"6px 12px\",\n border: \"none\",\n background: \"transparent\",\n color: theme === \"dark\" ? \"#d1d5db\" : \"#374151\",\n fontSize: 12,\n textAlign: \"left\" as const,\n cursor: \"pointer\",\n});\n\nexport function ExportMenu({\n theme,\n onExportPng,\n onExportSvg,\n onExportCsv,\n onExportJson,\n}: ExportMenuProps) {\n const [open, setOpen] = useState(false);\n\n const handleSelect = useCallback(\n (handler?: () => void) => {\n setOpen(false);\n handler?.();\n },\n [],\n );\n\n return (\n <div style={{ position: \"relative\", display: \"inline-block\" }}>\n <button\n type=\"button\"\n style={menuButtonStyle(theme)}\n onClick={() => setOpen(!open)}\n aria-label=\"Export\"\n aria-expanded={open}\n aria-haspopup=\"true\"\n >\n Export\n </button>\n {open && (\n <div style={dropdownStyle(theme)} role=\"menu\">\n {onExportPng && (\n <button\n type=\"button\"\n role=\"menuitem\"\n style={optionStyle(theme)}\n onClick={() => handleSelect(onExportPng)}\n >\n PNG Image\n </button>\n )}\n {onExportSvg && (\n <button\n type=\"button\"\n role=\"menuitem\"\n style={optionStyle(theme)}\n onClick={() => handleSelect(onExportSvg)}\n >\n SVG Vector\n </button>\n )}\n {onExportCsv && (\n <button\n type=\"button\"\n role=\"menuitem\"\n style={optionStyle(theme)}\n onClick={() => handleSelect(onExportCsv)}\n >\n CSV Data\n </button>\n )}\n {onExportJson && (\n <button\n type=\"button\"\n role=\"menuitem\"\n style={optionStyle(theme)}\n onClick={() => handleSelect(onExportJson)}\n >\n JSON Data\n </button>\n )}\n </div>\n )}\n </div>\n );\n}\n","/**\n * Spectrum comparison and mathematical operations.\n *\n * Provides functions for computing difference spectra, correlation\n * coefficients, spectral arithmetic, and residuals.\n *\n * All functions assume spectra share the same X-axis (same length\n * and point spacing). Use `interpolateToGrid` to align spectra\n * before comparison if needed.\n *\n * @module comparison\n */\n\nimport type { Spectrum } from \"../types\";\n\n/** Auto-incrementing ID counter for generated spectra. */\nlet idCounter = 0;\n\n/**\n * Compute the difference spectrum (a - b).\n *\n * @param a - First spectrum\n * @param b - Second spectrum\n * @returns New Spectrum representing a - b\n */\nexport function differenceSpectrum(a: Spectrum, b: Spectrum): Spectrum {\n const n = Math.min(a.y.length, b.y.length);\n const y = new Float64Array(n);\n for (let i = 0; i < n; i++) {\n y[i] = (a.y[i] as number) - (b.y[i] as number);\n }\n\n return {\n id: `diff-${++idCounter}`,\n label: `${a.label} − ${b.label}`,\n x: a.x.length <= b.x.length ? new Float64Array(a.x) : new Float64Array(b.x),\n y,\n xUnit: a.xUnit,\n yUnit: a.yUnit,\n color: \"#ef4444\",\n type: a.type,\n };\n}\n\n/**\n * Add two spectra element-wise.\n */\nexport function addSpectra(a: Spectrum, b: Spectrum): Spectrum {\n const n = Math.min(a.y.length, b.y.length);\n const y = new Float64Array(n);\n for (let i = 0; i < n; i++) {\n y[i] = (a.y[i] as number) + (b.y[i] as number);\n }\n\n return {\n id: `add-${++idCounter}`,\n label: `${a.label} + ${b.label}`,\n x: a.x.length <= b.x.length ? new Float64Array(a.x) : new Float64Array(b.x),\n y,\n xUnit: a.xUnit,\n yUnit: a.yUnit,\n type: a.type,\n };\n}\n\n/**\n * Multiply spectrum Y values by a scalar factor.\n */\nexport function scaleSpectrum(spectrum: Spectrum, factor: number): Spectrum {\n const n = spectrum.y.length;\n const y = new Float64Array(n);\n for (let i = 0; i < n; i++) {\n y[i] = (spectrum.y[i] as number) * factor;\n }\n\n return {\n id: `scaled-${++idCounter}`,\n label: `${spectrum.label} × ${factor}`,\n x: new Float64Array(spectrum.x),\n y,\n xUnit: spectrum.xUnit,\n yUnit: spectrum.yUnit,\n color: spectrum.color,\n type: spectrum.type,\n };\n}\n\n/**\n * Pearson correlation coefficient between two spectra's Y values.\n *\n * Returns a value in [-1, 1] where 1 means perfect positive\n * correlation and 0 means no linear correlation.\n */\nexport function correlationCoefficient(a: Spectrum, b: Spectrum): number {\n const n = Math.min(a.y.length, b.y.length);\n if (n === 0) return 0;\n\n let sumA = 0;\n let sumB = 0;\n for (let i = 0; i < n; i++) {\n sumA += a.y[i] as number;\n sumB += b.y[i] as number;\n }\n const meanA = sumA / n;\n const meanB = sumB / n;\n\n let cov = 0;\n let varA = 0;\n let varB = 0;\n for (let i = 0; i < n; i++) {\n const da = (a.y[i] as number) - meanA;\n const db = (b.y[i] as number) - meanB;\n cov += da * db;\n varA += da * da;\n varB += db * db;\n }\n\n const denom = Math.sqrt(varA * varB);\n if (denom === 0) return 0;\n return cov / denom;\n}\n\n/**\n * Compute the residual (absolute difference) between two spectra.\n */\nexport function residualSpectrum(a: Spectrum, b: Spectrum): Spectrum {\n const n = Math.min(a.y.length, b.y.length);\n const y = new Float64Array(n);\n for (let i = 0; i < n; i++) {\n y[i] = Math.abs((a.y[i] as number) - (b.y[i] as number));\n }\n\n return {\n id: `residual-${++idCounter}`,\n label: `|${a.label} − ${b.label}|`,\n x: a.x.length <= b.x.length ? new Float64Array(a.x) : new Float64Array(b.x),\n y,\n xUnit: a.xUnit,\n yUnit: a.yUnit,\n color: \"#f97316\",\n lineStyle: \"dashed\",\n type: a.type,\n };\n}\n\n/**\n * Interpolate a spectrum to a new X grid using linear interpolation.\n *\n * Useful for aligning two spectra that have different X-axis points\n * before performing comparison operations.\n *\n * @param spectrum - Source spectrum\n * @param newX - Target X values\n * @returns Spectrum interpolated to the new X grid\n */\nexport function interpolateToGrid(\n spectrum: Spectrum,\n newX: Float64Array | number[],\n): Spectrum {\n const n = Math.min(spectrum.x.length, spectrum.y.length);\n const m = newX.length;\n const y = new Float64Array(m);\n\n if (n < 2) return { ...spectrum, x: new Float64Array(newX), y };\n\n // Determine direction of source x (ascending or descending)\n const ascending = (spectrum.x[n - 1] as number) > (spectrum.x[0] as number);\n\n for (let j = 0; j < m; j++) {\n const targetX = newX[j] as number;\n\n // Find bracketing indices using binary search\n let lo = 0;\n let hi = n - 1;\n\n while (lo < hi - 1) {\n const mid = (lo + hi) >>> 1;\n if (ascending) {\n if ((spectrum.x[mid] as number) <= targetX) lo = mid;\n else hi = mid;\n } else {\n if ((spectrum.x[mid] as number) >= targetX) lo = mid;\n else hi = mid;\n }\n }\n\n // Linear interpolation\n const x0 = spectrum.x[lo] as number;\n const x1 = spectrum.x[hi] as number;\n const y0 = spectrum.y[lo] as number;\n const y1 = spectrum.y[hi] as number;\n\n if (x0 === x1) {\n y[j] = y0;\n } else {\n const t = (targetX - x0) / (x1 - x0);\n y[j] = y0 + t * (y1 - y0);\n }\n }\n\n return {\n ...spectrum,\n id: `interp-${++idCounter}`,\n x: new Float64Array(newX),\n y,\n };\n}\n","/**\n * SPC file parser for Thermo/Galactic spectral data format.\n *\n * Parses the binary SPC format used by GRAMS, Thermo Scientific,\n * PerkinElmer, and other spectroscopy software.\n *\n * Supports:\n * - Single and multi-spectrum files\n * - Even and uneven X spacing\n * - 32-bit float and 16-bit integer Y data\n * - File header metadata (resolution, instrument, etc.)\n *\n * Reference: \"The New Galactic SPC File Format\" specification\n *\n * @module spc\n */\n\nimport type { Spectrum, SpectrumType } from \"../types\";\n\n/** Auto-incrementing ID counter. */\nlet idCounter = 0;\n\n/** SPC file type flags. */\nconst TSPREC = 0x01; // Y data is 16-bit integer\nconst TMULTI = 0x04; // Multi-file (multiple spectra)\nconst TXVALS = 0x80; // Non-evenly spaced X data present\n\n/** SPC X-axis type codes to units. */\nconst X_TYPE_LABELS: Record<number, string> = {\n 0: \"Arbitrary\",\n 1: \"cm⁻¹\",\n 2: \"µm\",\n 3: \"nm\",\n 4: \"s\",\n 5: \"min\",\n 6: \"Hz\",\n 7: \"kHz\",\n 8: \"MHz\",\n 9: \"m/z\",\n 10: \"Da\",\n 11: \"ppm\",\n 12: \"days\",\n 13: \"years\",\n 14: \"Raman shift (cm⁻¹)\",\n 15: \"eV\",\n 16: \"Text label\",\n 255: \"Double interferogram\",\n};\n\n/** SPC Y-axis type codes to units. */\nconst Y_TYPE_LABELS: Record<number, string> = {\n 0: \"Arbitrary\",\n 1: \"Interferogram\",\n 2: \"Absorbance\",\n 3: \"Kubelka-Munk\",\n 4: \"Counts\",\n 5: \"V\",\n 6: \"°\",\n 7: \"mA\",\n 8: \"mm\",\n 9: \"mV\",\n 10: \"log(1/R)\",\n 11: \"%\",\n 12: \"Intensity\",\n 13: \"Relative intensity\",\n 14: \"Energy\",\n 16: \"dB\",\n 19: \"°C\",\n 20: \"°F\",\n 21: \"K\",\n 22: \"Index of refraction [n]\",\n 23: \"Extinction coeff. [k]\",\n 24: \"Real\",\n 25: \"Imaginary\",\n 26: \"Complex\",\n 128: \"Transmittance\",\n 129: \"Reflectance\",\n 130: \"Arbitrary (Valley to peak)\",\n 131: \"Emission\",\n};\n\n/** Infer SpectrumType from SPC X-type code. */\nfunction inferSpectrumType(xType: number, yType: number): SpectrumType {\n if (xType === 1) return \"IR\"; // cm⁻¹\n if (xType === 14) return \"Raman\";\n if (xType === 3 && (yType === 2 || yType === 128)) return \"UV-Vis\";\n if (xType === 2) return \"NIR\"; // µm\n if (yType === 131) return \"fluorescence\";\n return \"other\";\n}\n\n/**\n * Parse an SPC binary file into Spectrum objects.\n *\n * @param buffer - ArrayBuffer containing the SPC file data\n * @returns Array of parsed Spectrum objects\n * @throws Error if the file is not a valid SPC file\n */\nexport function parseSpc(buffer: ArrayBuffer): Spectrum[] {\n const view = new DataView(buffer);\n const minSize = 512; // SPC header is 512 bytes\n\n if (buffer.byteLength < minSize) {\n throw new Error(\"Invalid SPC file: too small for SPC header\");\n }\n\n // Read main file header (512 bytes)\n const flags = view.getUint8(0);\n const fileVersion = view.getUint8(1);\n\n // Validate version (0x4B = new format, 0x4D = old format)\n if (fileVersion !== 0x4b && fileVersion !== 0x4d) {\n throw new Error(\n `Unsupported SPC version: 0x${fileVersion.toString(16)}. Expected 0x4B or 0x4D.`,\n );\n }\n\n const xType = view.getUint8(2);\n const yType = view.getUint8(3);\n const npoints = view.getUint32(4, true); // little-endian\n const firstX = view.getFloat64(8, true);\n const lastX = view.getFloat64(16, true);\n const numSpectra = view.getUint32(24, true);\n\n // Experiment type at offset 28\n const xUnit = X_TYPE_LABELS[xType] ?? \"Arbitrary\";\n const yUnit = Y_TYPE_LABELS[yType] ?? \"Arbitrary\";\n\n // Read memo string (offset 30, 130 bytes max)\n const memoBytes = new Uint8Array(buffer, 30, 130);\n const memo = decodeText(memoBytes);\n\n const isMulti = (flags & TMULTI) !== 0;\n const hasXValues = (flags & TXVALS) !== 0;\n const is16Bit = (flags & TSPREC) !== 0;\n const specType = inferSpectrumType(xType, yType);\n\n // Generate evenly-spaced X values if not provided\n let sharedX: Float64Array | null = null;\n\n if (!hasXValues && npoints > 0) {\n sharedX = new Float64Array(npoints);\n const step = npoints > 1 ? (lastX - firstX) / (npoints - 1) : 0;\n for (let i = 0; i < npoints; i++) {\n sharedX[i] = firstX + i * step;\n }\n }\n\n const spectra: Spectrum[] = [];\n let offset = 512; // Start after main header\n\n // Read X values if present (only for non-multi or old format)\n let fileXValues: Float64Array | null = null;\n if (hasXValues && !isMulti) {\n fileXValues = new Float64Array(npoints);\n for (let i = 0; i < npoints; i++) {\n fileXValues[i] = view.getFloat32(offset, true);\n offset += 4;\n }\n }\n\n const count = isMulti ? numSpectra : 1;\n\n for (let s = 0; s < count; s++) {\n let xVals: Float64Array;\n let yVals: Float64Array;\n let subNpoints = npoints;\n\n if (isMulti) {\n // Read sub-file header (32 bytes)\n if (offset + 32 > buffer.byteLength) break;\n\n // Sub-header fields: flags(1), exp(1), index(2), startX(4), endX(4), npoints(4), ...\n const subStartX = view.getFloat32(offset + 4, true);\n const subEndX = view.getFloat32(offset + 8, true);\n subNpoints = view.getUint32(offset + 12, true) || npoints;\n offset += 32;\n\n // Sub-file X values\n if (hasXValues) {\n xVals = new Float64Array(subNpoints);\n for (let i = 0; i < subNpoints; i++) {\n if (offset + 4 > buffer.byteLength) break;\n xVals[i] = view.getFloat32(offset, true);\n offset += 4;\n }\n } else if (sharedX) {\n xVals = sharedX;\n } else {\n // Generate from sub-header\n xVals = new Float64Array(subNpoints);\n const step = subNpoints > 1 ? (subEndX - subStartX) / (subNpoints - 1) : 0;\n for (let i = 0; i < subNpoints; i++) {\n xVals[i] = subStartX + i * step;\n }\n }\n } else {\n xVals = fileXValues ?? sharedX ?? new Float64Array(0);\n }\n\n // Read Y values\n yVals = new Float64Array(subNpoints);\n\n if (is16Bit) {\n for (let i = 0; i < subNpoints; i++) {\n if (offset + 2 > buffer.byteLength) break;\n yVals[i] = view.getInt16(offset, true);\n offset += 2;\n }\n } else {\n for (let i = 0; i < subNpoints; i++) {\n if (offset + 4 > buffer.byteLength) break;\n yVals[i] = view.getFloat32(offset, true);\n offset += 4;\n }\n }\n\n spectra.push({\n id: `spc-${++idCounter}`,\n label: memo || `SPC Spectrum ${s + 1}`,\n x: xVals,\n y: yVals,\n xUnit,\n yUnit,\n type: specType,\n meta: {\n format: \"SPC\",\n version: fileVersion === 0x4b ? \"new\" : \"old\",\n xType: xType.toString(),\n yType: yType.toString(),\n },\n });\n }\n\n if (spectra.length === 0) {\n throw new Error(\"Invalid SPC file: no spectra found\");\n }\n\n return spectra;\n}\n\n/** Decode a null-terminated byte array to string. */\nfunction decodeText(bytes: Uint8Array): string {\n const nullIdx = bytes.indexOf(0);\n const slice = nullIdx >= 0 ? bytes.slice(0, nullIdx) : bytes;\n return new TextDecoder(\"ascii\").decode(slice).trim();\n}\n"],"mappings":"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","names":["index_exports","__export","AnnotationLayer","AxisLayer","Crosshair","DARK_THEME","DropZone","ExportMenu","KEYBOARD_SHORTCUTS","LIGHT_THEME","LINE_DASH_PATTERNS","Legend","Minimap","PeakMarkers","RegionSelector","SPECTRUM_COLORS","SpectraView","SpectrumCanvas","StackedView","Toolbar","Tooltip","addSpectra","baselineRubberBand","binarySearchClosest","computeXExtent","computeYExtent","correlationCoefficient","createXScale","createYScale","derivative1st","derivative2nd","detectPeaks","differenceSpectrum","downloadSvg","generateChartDescription","generateSvg","getSpectrumColor","getThemeColors","interpolateToGrid","lttbDownsample","normalizeArea","normalizeMinMax","normalizeSNV","parseCsv","parseCsvMulti","parseJcamp","parseJson","parseSpc","prefersReducedMotion","residualSpectrum","scaleSpectrum","smoothSavitzkyGolay","snapToNearestSpectrum","useExport","useHistory","useKeyboardNavigation","useNormalization","usePeakPicking","useRegionSelect","useResizeObserver","useSpectrumData","useZoomPan","__toCommonJS","import_react","import_d3_scale","import_d3_array","Y_PADDING","computeXExtent","spectra","globalMin","globalMax","s","min","max","computeYExtent","pad","createXScale","domain","width","margin","reverseX","plotWidth","d","createYScale","height","plotHeight","SPECTRUM_COLORS","LIGHT_THEME","DARK_THEME","getSpectrumColor","index","getThemeColors","theme","import_react","import_d3_zoom","import_d3_selection","import_d3_transition","ZOOM_STEP","useZoomPan","options","plotWidth","plotHeight","xScale","yScale","scaleExtent","enabled","onViewChange","zoomRef","zoomBehaviorRef","onViewChangeRef","scaleExtentRef","transform","setTransform","zoomedXScale","zoomedYScale","element","zoomBehavior","event","newTransform","newXScale","newYScale","resetZoom","zoomIn","zoomOut","import_react","lttbDownsample","x","y","startIdx","endIdx","xScale","yScale","targetCount","n","result","i","bucketCount","bucketSize","prevSelectedIdx","bucket","bucketStart","bucketEnd","nextBucketStart","nextBucketEnd","avgX","avgY","avgCount","prevPx","prevPy","maxArea","bestIdx","px","py","area","LINE_WIDTH","CANVAS_DASH_PATTERNS","DECIMATION_THRESHOLD","clearCanvas","ctx","width","height","drawSpectrum","spectrum","index","xScale","yScale","plotWidth","options","highlighted","opacity","n","color","getSpectrumColor","baseWidth","lineWidth","dashPattern","xMin","xMax","domainMin","domainMax","startIdx","endIdx","i","visibleCount","targetPoints","points","lttbDownsample","started","px","py","drawAllSpectra","spectra","highlightedId","import_jsx_runtime","SpectrumCanvas","spectra","xScale","yScale","width","height","highlightedId","ref","canvasRef","dprRef","canvas","dpr","ctx","drawAllSpectra","import_jsx_runtime","generateTicks","scale","count","d0","d1","min","step","_","i","formatTick","value","AxisLayer","xScale","yScale","width","height","xLabel","yLabel","showGrid","colors","xTicks","yTicks","tick","import_jsx_runtime","PeakMarkers","peaks","xScale","yScale","colors","onPeakClick","xMin","xMax","domainMin","domainMax","visiblePeaks","p","peak","i","px","py","import_jsx_runtime","RegionSelector","regions","xScale","height","colors","region","i","x1","x2","left","w","import_jsx_runtime","Crosshair","position","width","height","colors","snapPoint","formatValue","v","import_jsx_runtime","AnnotationLayer","annotations","xScale","yScale","colors","ann","px","py","dx","dy","textX","textY","fontSize","color","showLine","binarySearchClosest","arr","target","length","ascending","lo","hi","mid","midVal","dLo","dHi","snapToNearestSpectrum","spectra","dataX","cursorPy","xScale","yScale","best","spectrum","n","idx","sx","sy","pxDist","pyDist","distance","import_react","import_jsx_runtime","buttonStyle","theme","toolbarStyle","Toolbar","onZoomIn","onZoomOut","onReset","isZoomed","import_react","import_jsx_runtime","containerStyle","theme","position","itemStyle","isHidden","isHighlighted","swatchStyle","color","Legend","spectra","onToggleVisibility","onHighlight","highlightedId","s","i","getSpectrumColor","import_react","import_jsx_runtime","DropZone","enabled","theme","width","height","onDrop","children","isDragging","setIsDragging","dragCountRef","handleDragEnter","e","handleDragLeave","handleDragOver","handleDrop","files","import_react","import_jsx_runtime","PANEL_GAP","StackedView","spectra","xScale","plotWidth","plotHeight","margin","theme","showGrid","xLabel","yLabel","visible","s","colors","getThemeColors","panelCount","totalGap","panelHeight","spectrum","i","yOffset","yExtent","computeYExtent","yScale","createYScale","color","getSpectrumColor","coloredSpectrum","AxisLayer","SpectrumCanvas","import_react","useRegionSelect","options","enabled","xScale","onRegionSelect","pendingRegion","setPendingRegion","dragStartRef","handleMouseDown","event","rect","px","dataX","handleMouseMove","start","handleMouseUp","import_react","useResizeObserver","size","setSize","observerRef","elementRef","ref","node","observer","entries","entry","width","height","import_react","useKeyboardNavigation","options","onZoomIn","onZoomOut","onReset","enabled","event","prefersReducedMotion","generateChartDescription","spectrumCount","xLabel","yLabel","KEYBOARD_SHORTCUTS","import_jsx_runtime","DEFAULT_MARGIN","DEFAULT_WIDTH","DEFAULT_HEIGHT","resolveConfig","props","inferLabels","spectra","xLabel","yLabel","first","SpectraView","peaks","regions","annotations","onPeakClick","onViewChange","onCrosshairMove","onToggleVisibility","onFileDrop","onRegionSelect","canvasRef","snapCrosshair","resizeRef","measuredSize","useResizeObserver","clipId","config","width","height","margin","reverseX","theme","plotWidth","plotHeight","colors","getThemeColors","labels","xExtent","computeXExtent","yExtent","computeYExtent","baseXScale","createXScale","baseYScale","createYScale","onViewChangeRef","stableOnViewChange","xDomain","yDomain","zoomRef","zoomState","zoomedXScale","zoomedYScale","resetZoom","zoomIn","zoomOut","useZoomPan","pendingRegion","regionMouseDown","regionMouseMove","regionMouseUp","useRegionSelect","highlightedId","setHighlightedId","crosshairPos","setCrosshairPos","snapPointState","setSnapPointState","onCrosshairMoveRef","handleMouseMove","event","rect","px","py","dataX","dataY","snap","snapToNearestSpectrum","spIdx","s","getSpectrumColor","handleMouseLeave","handleKeyDown","useKeyboardNavigation","chartDescription","generateChartDescription","isStacked","toolbarHeight","Toolbar","Legend","DropZone","StackedView","SpectrumCanvas","AxisLayer","RegionSelector","PeakMarkers","AnnotationLayer","Crosshair","e","import_react","import_d3_scale","import_jsx_runtime","Minimap","spectra","xExtent","yExtent","visibleXDomain","width","height","theme","isZoomed","canvasRef","colors","getThemeColors","xScale","yScale","ctx","s","spectrum","n","color","getSpectrumColor","step","started","i","px","py","vpLeft","vpRight","vpWidth","import_react","import_jsx_runtime","formatNumber","v","format","Tooltip","data","spectra","peaks","plotWidth","plotHeight","colors","numberFormat","entries","s","spectrum","i","n","idx","binarySearchClosest","getSpectrumColor","nearestPeak","best","bestDist","peak","dist","lineHeight","headerHeight","peakLineHeight","tooltipHeight","tooltipWidth","tx","ty","entry","import_react","detectPeaks","x","y","options","prominence","minDistance","maxPeaks","yMin","yMax","i","signalRange","absProminence","candidates","leftMin","findMinBefore","rightMin","findMinAfter","prom","a","b","kept","c","k","formatWavenumber","min","j","value","usePeakPicking","spectra","options","enabled","spectrumIds","prominence","minDistance","maxPeaks","targetSpectra","s","allPeaks","spectrum","peaks","detectPeaks","peak","import_react","idCounter","DELIMITER_CANDIDATES","detectDelimiter","text","lines","bestDelimiter","bestScore","d","counts","line","minCount","c","parseCsv","options","xColumn","yColumn","hasHeader","label","delimiter","headerLabel","startIndex","headers","h","xValues","yValues","i","parts","xVal","yVal","parseCsvMulti","numColumns","yArrays","col","xArray","yArr","idCounter","parseJson","text","data","item","i","parseSingleJson","obj","input","index","xRaw","yRaw","label","idCounter","converterModule","converterChecked","getConverter","inferType","info","dataType","parseJcamp","text","converter","parseWithConverter","parseBasicJcamp","entry","i","firstSpectrum","lines","xValues","yValues","inData","line","trimmed","match","key","value","values","x0","firstX","lastX","npoints","deltaX","j","detectFormat","filename","useSpectrumData","initialSpectra","spectra","setSpectra","loading","setLoading","error","setError","loadText","text","format","parsed","parseJcamp","parseCsv","parseJson","prev","err","message","loadFile","file","addSpectrum","spectrum","removeSpectrum","id","s","toggleVisibility","clear","import_react","LINE_DASH_PATTERNS","generateSvg","spectra","xScale","yScale","options","width","height","background","title","paths","s","i","color","getSpectrumColor","lineStyle","lineWidth","dashArray","n","points","j","px","py","downloadSvg","svg","filename","blob","url","a","downloadBlob","blob","filename","url","a","useExport","exportPng","canvas","exportCsv","spectra","visible","s","header","rows","x","i","csv","maxLen","values","exportJson","output","json","exportSvg","xScale","yScale","width","height","svg","generateSvg","downloadSvg","import_react","baselineRubberBand","y","n","hullIndices","i","j","a","b","baseline","hi","t","result","normalizeMinMax","min","max","v","range","normalizeArea","x","area","normalizeSNV","sum","mean","variance","d","std","smoothSavitzkyGolay","windowSize","w","halfW","coefficients","getSGCoefficients","precomputed","derivative1st","derivative2nd","dx1","dx2","dxAvg","MODE_LABELS","transformSpectrum","spectrum","mode","smoothWindow","newY","normalizeMinMax","normalizeArea","normalizeSNV","baselineRubberBand","smoothSavitzkyGolay","derivative1st","useNormalization","spectra","s","import_react","useHistory","initialState","maxDepth","state","setState","undoStack","redoStack","undoCount","setUndoCount","redoCount","setRedoCount","push","newState","prev","undo","current","redo","next","reset","import_react","import_jsx_runtime","menuButtonStyle","theme","dropdownStyle","optionStyle","ExportMenu","onExportPng","onExportSvg","onExportCsv","onExportJson","open","setOpen","handleSelect","handler","idCounter","differenceSpectrum","a","b","n","y","i","addSpectra","scaleSpectrum","spectrum","factor","correlationCoefficient","sumA","sumB","meanA","meanB","cov","varA","varB","da","db","denom","residualSpectrum","interpolateToGrid","newX","m","ascending","j","targetX","lo","hi","mid","x0","x1","y0","y1","t","idCounter","TSPREC","TMULTI","TXVALS","X_TYPE_LABELS","Y_TYPE_LABELS","inferSpectrumType","xType","yType","parseSpc","buffer","view","flags","fileVersion","npoints","firstX","lastX","numSpectra","xUnit","yUnit","memoBytes","memo","decodeText","isMulti","hasXValues","is16Bit","specType","sharedX","step","i","spectra","offset","fileXValues","count","s","xVals","yVals","subNpoints","subStartX","subEndX","bytes","nullIdx","slice"]}
|