spectraview 0.1.0 → 1.0.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.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as react from 'react';
3
+ import { ReactNode } from 'react';
2
4
  import * as d3_scale from 'd3-scale';
3
5
  import { ScaleLinear } from 'd3-scale';
4
6
  import { ZoomTransform } from 'd3-zoom';
@@ -10,6 +12,8 @@ import { ZoomTransform } from 'd3-zoom';
10
12
  */
11
13
  /** Supported spectral technique types. */
12
14
  type SpectrumType = "IR" | "Raman" | "NIR" | "UV-Vis" | "fluorescence" | "other";
15
+ /** Supported line styles for spectrum rendering. */
16
+ type LineStyle = "solid" | "dashed" | "dotted" | "dash-dot";
13
17
  /** A single spectrum dataset. */
14
18
  interface Spectrum {
15
19
  /** Unique identifier. */
@@ -28,6 +32,10 @@ interface Spectrum {
28
32
  type?: SpectrumType;
29
33
  /** Rendering color (CSS color string). */
30
34
  color?: string;
35
+ /** Line style for rendering. */
36
+ lineStyle?: LineStyle;
37
+ /** Line width in pixels. */
38
+ lineWidth?: number;
31
39
  /** Whether this spectrum is visible. */
32
40
  visible?: boolean;
33
41
  /** Arbitrary metadata from file headers. */
@@ -55,6 +63,25 @@ interface Region {
55
63
  /** Optional fill color. */
56
64
  color?: string;
57
65
  }
66
+ /** A text annotation placed on the chart. */
67
+ interface Annotation {
68
+ /** Unique identifier. */
69
+ id: string;
70
+ /** Data-space x position (anchor point). */
71
+ x: number;
72
+ /** Data-space y position (anchor point). */
73
+ y: number;
74
+ /** Annotation text. */
75
+ text: string;
76
+ /** Pixel offset from anchor point [dx, dy]. Defaults to [0, -20]. */
77
+ offset?: [number, number];
78
+ /** Font size in pixels. Defaults to 11. */
79
+ fontSize?: number;
80
+ /** Text color (CSS). Defaults to theme text color. */
81
+ color?: string;
82
+ /** Show anchor line from text to data point. Defaults to true. */
83
+ showAnchorLine?: boolean;
84
+ }
58
85
  /** Current zoom/pan view state. */
59
86
  interface ViewState {
60
87
  /** Visible x-axis domain [min, max]. */
@@ -65,7 +92,9 @@ interface ViewState {
65
92
  /** Theme configuration. */
66
93
  type Theme = "light" | "dark";
67
94
  /** Display mode for multiple spectra. */
68
- type DisplayMode = "overlay";
95
+ type DisplayMode = "overlay" | "stacked";
96
+ /** Legend position relative to the chart. */
97
+ type LegendPosition$1 = "top" | "bottom" | "left" | "right";
69
98
  /** Margin configuration for the chart area. */
70
99
  interface Margin {
71
100
  top: number;
@@ -93,6 +122,10 @@ interface SpectraViewProps {
93
122
  peaks?: Peak[];
94
123
  /** Highlighted regions. */
95
124
  regions?: Region[];
125
+ /** Text annotations to display on the chart. */
126
+ annotations?: Annotation[];
127
+ /** Snap crosshair to nearest spectrum data point. Defaults to true. */
128
+ snapCrosshair?: boolean;
96
129
  /** X-axis label override. */
97
130
  xLabel?: string;
98
131
  /** Y-axis label override. */
@@ -103,8 +136,26 @@ interface SpectraViewProps {
103
136
  margin?: Partial<Margin>;
104
137
  /** Theme. */
105
138
  theme?: Theme;
139
+ /** Show legend (auto-shown when >1 spectrum). */
140
+ showLegend?: boolean;
141
+ /** Legend position. */
142
+ legendPosition?: LegendPosition$1;
143
+ /** Callback when a spectrum's visibility is toggled via legend. */
144
+ onToggleVisibility?: (id: string) => void;
145
+ /** Enable drag-and-drop file loading. */
146
+ enableDragDrop?: boolean;
147
+ /** Callback when files are dropped. */
148
+ onFileDrop?: (files: File[]) => void;
149
+ /** Enable interactive region selection (Shift+drag). */
150
+ enableRegionSelect?: boolean;
151
+ /** Callback when a region is created. */
152
+ onRegionSelect?: (region: Region) => void;
153
+ /** Responsive sizing (fills container width). */
154
+ responsive?: boolean;
106
155
  /** Custom CSS class name. */
107
156
  className?: string;
157
+ /** Ref to access the underlying Canvas element (for export). */
158
+ canvasRef?: React.RefObject<HTMLCanvasElement | null>;
108
159
  /** Callback when user clicks a peak marker. */
109
160
  onPeakClick?: (peak: Peak) => void;
110
161
  /** Callback when zoom/pan state changes. */
@@ -120,9 +171,14 @@ interface ResolvedConfig {
120
171
  showGrid: boolean;
121
172
  showCrosshair: boolean;
122
173
  showToolbar: boolean;
174
+ showLegend: boolean;
175
+ legendPosition: LegendPosition$1;
123
176
  displayMode: DisplayMode;
124
177
  margin: Margin;
125
178
  theme: Theme;
179
+ responsive: boolean;
180
+ enableDragDrop: boolean;
181
+ enableRegionSelect: boolean;
126
182
  }
127
183
 
128
184
  declare function SpectraView(props: SpectraViewProps): react_jsx_runtime.JSX.Element;
@@ -141,7 +197,7 @@ interface SpectrumCanvasProps {
141
197
  /** ID of the currently highlighted spectrum. */
142
198
  highlightedId?: string;
143
199
  }
144
- declare function SpectrumCanvas({ spectra, xScale, yScale, width, height, highlightedId, }: SpectrumCanvasProps): react_jsx_runtime.JSX.Element;
200
+ declare const SpectrumCanvas: react.ForwardRefExoticComponent<SpectrumCanvasProps & react.RefAttributes<HTMLCanvasElement>>;
145
201
 
146
202
  /**
147
203
  * Default color palette for rendering multiple spectra.
@@ -237,6 +293,19 @@ interface CrosshairPosition {
237
293
  /** Data-space y value. */
238
294
  dataY: number;
239
295
  }
296
+ /** Snap point data for rendering a snap dot on the nearest spectrum. */
297
+ interface SnapPoint {
298
+ /** Pixel x of the snapped data point. */
299
+ px: number;
300
+ /** Pixel y of the snapped data point. */
301
+ py: number;
302
+ /** Data-space x value. */
303
+ dataX: number;
304
+ /** Data-space y value. */
305
+ dataY: number;
306
+ /** Color of the spectrum (for dot fill). */
307
+ color?: string;
308
+ }
240
309
  interface CrosshairProps {
241
310
  /** Current crosshair position, or null when not hovering. */
242
311
  position: CrosshairPosition | null;
@@ -246,8 +315,10 @@ interface CrosshairProps {
246
315
  height: number;
247
316
  /** Theme colors. */
248
317
  colors: ThemeColors;
318
+ /** Optional snap point on nearest spectrum. */
319
+ snapPoint?: SnapPoint | null;
249
320
  }
250
- declare function Crosshair({ position, width, height, colors, }: CrosshairProps): react_jsx_runtime.JSX.Element | null;
321
+ declare function Crosshair({ position, width, height, colors, snapPoint, }: CrosshairProps): react_jsx_runtime.JSX.Element | null;
251
322
 
252
323
  interface ToolbarProps {
253
324
  /** Zoom in handler. */
@@ -261,7 +332,53 @@ interface ToolbarProps {
261
332
  /** Theme. */
262
333
  theme: Theme;
263
334
  }
264
- declare function Toolbar({ onZoomIn, onZoomOut, onReset, isZoomed, theme, }: ToolbarProps): react_jsx_runtime.JSX.Element;
335
+ declare const Toolbar: react.NamedExoticComponent<ToolbarProps>;
336
+
337
+ /** Position for the legend relative to the chart. */
338
+ type LegendPosition = "top" | "bottom" | "left" | "right";
339
+ interface LegendProps {
340
+ /** Spectra to list in the legend. */
341
+ spectra: Spectrum[];
342
+ /** Theme for styling. */
343
+ theme: Theme;
344
+ /** Legend position. */
345
+ position: LegendPosition;
346
+ /** Callback when a spectrum's visibility is toggled. */
347
+ onToggleVisibility?: (id: string) => void;
348
+ /** Callback when hovering a spectrum in the legend. */
349
+ onHighlight?: (id: string | null) => void;
350
+ /** Currently highlighted spectrum ID. */
351
+ highlightedId?: string | null;
352
+ }
353
+ declare const Legend: react.NamedExoticComponent<LegendProps>;
354
+
355
+ interface DropZoneProps {
356
+ /** Whether drag-drop is enabled. */
357
+ enabled: boolean;
358
+ /** Theme for styling the overlay. */
359
+ theme: Theme;
360
+ /** Width of the drop zone. */
361
+ width: number;
362
+ /** Height of the drop zone. */
363
+ height: number;
364
+ /** Callback when files are dropped. */
365
+ onDrop?: (files: File[]) => void;
366
+ /** Children to render inside the drop zone. */
367
+ children: ReactNode;
368
+ }
369
+ declare function DropZone({ enabled, theme, width, height, onDrop, children, }: DropZoneProps): react_jsx_runtime.JSX.Element;
370
+
371
+ interface AnnotationLayerProps {
372
+ /** Annotations to render. */
373
+ annotations: Annotation[];
374
+ /** X scale (zoomed). */
375
+ xScale: ScaleLinear<number, number>;
376
+ /** Y scale (zoomed). */
377
+ yScale: ScaleLinear<number, number>;
378
+ /** Theme colors. */
379
+ colors: ThemeColors;
380
+ }
381
+ declare function AnnotationLayer({ annotations, xScale, yScale, colors, }: AnnotationLayerProps): react_jsx_runtime.JSX.Element | null;
265
382
 
266
383
  /**
267
384
  * Hook for zoom and pan behavior backed by d3-zoom.
@@ -396,6 +513,8 @@ declare function useSpectrumData(initialSpectra?: Spectrum[]): UseSpectrumDataRe
396
513
  interface UseExportReturn {
397
514
  /** Export the canvas as a PNG data URL. */
398
515
  exportPng: (canvas: HTMLCanvasElement, filename?: string) => void;
516
+ /** Export visible spectra as SVG vector. */
517
+ exportSvg: (spectra: Spectrum[], xScale: ScaleLinear<number, number>, yScale: ScaleLinear<number, number>, width: number, height: number, filename?: string) => void;
399
518
  /** Export visible spectra as CSV text. */
400
519
  exportCsv: (spectra: Spectrum[], filename?: string) => void;
401
520
  /** Export visible spectra as JSON. */
@@ -406,6 +525,222 @@ interface UseExportReturn {
406
525
  */
407
526
  declare function useExport(): UseExportReturn;
408
527
 
528
+ /**
529
+ * Hook for interactive region selection via Shift+drag.
530
+ *
531
+ * Tracks mouse drag state when Shift is held, converting pixel
532
+ * coordinates to data-space values using the provided x-scale.
533
+ */
534
+
535
+ interface UseRegionSelectOptions {
536
+ /** Whether region selection is enabled. */
537
+ enabled: boolean;
538
+ /** X-axis scale for pixel-to-data conversion. */
539
+ xScale: ScaleLinear<number, number>;
540
+ /** Callback when a region is created. */
541
+ onRegionSelect?: (region: Region) => void;
542
+ }
543
+ interface UseRegionSelectReturn {
544
+ /** Pending region being dragged (null when not dragging). */
545
+ pendingRegion: Region | null;
546
+ /** Mouse down handler — call on the interaction rect. */
547
+ handleMouseDown: (event: React.MouseEvent<SVGRectElement>) => void;
548
+ /** Mouse move handler — call on the interaction rect. */
549
+ handleMouseMove: (event: React.MouseEvent<SVGRectElement>) => void;
550
+ /** Mouse up handler — call on the interaction rect. */
551
+ handleMouseUp: () => void;
552
+ }
553
+ declare function useRegionSelect(options: UseRegionSelectOptions): UseRegionSelectReturn;
554
+
555
+ /**
556
+ * Hook for responsive sizing via ResizeObserver.
557
+ *
558
+ * Tracks the width (and optionally height) of a container element,
559
+ * enabling the chart to auto-size to its parent.
560
+ */
561
+ interface ResizeObserverSize {
562
+ width: number;
563
+ height: number;
564
+ }
565
+ declare function useResizeObserver(): {
566
+ ref: React.RefCallback<HTMLElement>;
567
+ size: ResizeObserverSize | null;
568
+ };
569
+
570
+ /**
571
+ * Hook for keyboard navigation within the spectrum viewer.
572
+ *
573
+ * Handles arrow keys for panning, +/- for zoom, Escape for reset.
574
+ */
575
+ interface UseKeyboardNavigationOptions {
576
+ /** Zoom in handler. */
577
+ onZoomIn: () => void;
578
+ /** Zoom out handler. */
579
+ onZoomOut: () => void;
580
+ /** Reset zoom handler. */
581
+ onReset: () => void;
582
+ /** Whether keyboard navigation is enabled. */
583
+ enabled?: boolean;
584
+ }
585
+ declare function useKeyboardNavigation(options: UseKeyboardNavigationOptions): (event: React.KeyboardEvent) => void;
586
+
587
+ interface StackedViewProps {
588
+ /** Spectra to display. */
589
+ spectra: Spectrum[];
590
+ /** Zoomed x-scale (shared across panels). */
591
+ xScale: ScaleLinear<number, number>;
592
+ /** Full plot width. */
593
+ plotWidth: number;
594
+ /** Full plot height (will be divided among panels). */
595
+ plotHeight: number;
596
+ /** Chart margins. */
597
+ margin: Margin;
598
+ /** Theme. */
599
+ theme: Theme;
600
+ /** Show grid lines. */
601
+ showGrid: boolean;
602
+ /** X-axis label. */
603
+ xLabel: string;
604
+ /** Y-axis label. */
605
+ yLabel: string;
606
+ }
607
+ declare function StackedView({ spectra, xScale, plotWidth, plotHeight, margin, theme, showGrid, xLabel, yLabel, }: StackedViewProps): react_jsx_runtime.JSX.Element;
608
+
609
+ interface ExportMenuProps {
610
+ /** Theme for styling. */
611
+ theme: Theme;
612
+ /** Export as PNG. */
613
+ onExportPng?: () => void;
614
+ /** Export as SVG. */
615
+ onExportSvg?: () => void;
616
+ /** Export as CSV. */
617
+ onExportCsv?: () => void;
618
+ /** Export as JSON. */
619
+ onExportJson?: () => void;
620
+ }
621
+ declare function ExportMenu({ theme, onExportPng, onExportSvg, onExportCsv, onExportJson, }: ExportMenuProps): react_jsx_runtime.JSX.Element;
622
+
623
+ /**
624
+ * SVG export utility for generating publication-quality vector figures.
625
+ *
626
+ * Serializes a chart's SVG element to a standalone SVG string with
627
+ * embedded spectral data paths.
628
+ */
629
+
630
+ /** Line dash patterns for different line styles. */
631
+ declare const LINE_DASH_PATTERNS: Record<string, string>;
632
+ interface SvgExportOptions {
633
+ /** Width of the SVG. */
634
+ width: number;
635
+ /** Height of the SVG. */
636
+ height: number;
637
+ /** Background color. */
638
+ background?: string;
639
+ /** Title text for the SVG. */
640
+ title?: string;
641
+ }
642
+ /**
643
+ * Generate an SVG string for the given spectra.
644
+ */
645
+ declare function generateSvg(spectra: Spectrum[], xScale: ScaleLinear<number, number>, yScale: ScaleLinear<number, number>, options: SvgExportOptions): string;
646
+ /**
647
+ * Download an SVG string as a file.
648
+ */
649
+ declare function downloadSvg(svg: string, filename?: string): void;
650
+
651
+ /**
652
+ * Accessibility utilities for SpectraView.
653
+ *
654
+ * Provides helpers for reduced motion detection, ARIA label generation,
655
+ * and keyboard navigation constants.
656
+ */
657
+ /** Check if the user prefers reduced motion. */
658
+ declare function prefersReducedMotion(): boolean;
659
+ /** Generate an accessible description for a spectrum chart. */
660
+ declare function generateChartDescription(spectrumCount: number, xLabel: string, yLabel: string): string;
661
+ /** Keyboard shortcut definitions. */
662
+ declare const KEYBOARD_SHORTCUTS: {
663
+ readonly PAN_LEFT: "ArrowLeft";
664
+ readonly PAN_RIGHT: "ArrowRight";
665
+ readonly PAN_UP: "ArrowUp";
666
+ readonly PAN_DOWN: "ArrowDown";
667
+ readonly ZOOM_IN: "+";
668
+ readonly ZOOM_IN_ALT: "=";
669
+ readonly ZOOM_OUT: "-";
670
+ readonly RESET: "Escape";
671
+ readonly NEXT_PEAK: "Tab";
672
+ readonly PREV_PEAK: "Shift+Tab";
673
+ };
674
+
675
+ /**
676
+ * Binary search utilities for snapping crosshair to nearest spectrum data.
677
+ *
678
+ * Given a cursor x-position, finds the closest data point on visible spectra
679
+ * using binary search for O(log n) performance.
680
+ */
681
+
682
+ /** Result of a snap-to-spectrum search. */
683
+ interface SnapResult {
684
+ /** Spectrum ID of the closest match. */
685
+ spectrumId: string;
686
+ /** Index within the spectrum's data arrays. */
687
+ index: number;
688
+ /** Data-space x value of the snapped point. */
689
+ x: number;
690
+ /** Data-space y value of the snapped point. */
691
+ y: number;
692
+ /** Pixel distance from cursor to the snapped point. */
693
+ distance: number;
694
+ }
695
+ /**
696
+ * Binary search for the index of the closest x-value in a sorted array.
697
+ *
698
+ * Works with both ascending and descending arrays.
699
+ * Returns the index of the element closest to `target`.
700
+ */
701
+ declare function binarySearchClosest(arr: Float64Array | number[], target: number, length: number): number;
702
+ /**
703
+ * Find the nearest data point across all visible spectra to a given
704
+ * data-space x position. Uses pixel-space distance for ranking.
705
+ */
706
+ declare function snapToNearestSpectrum(spectra: Spectrum[], dataX: number, cursorPy: number, xScale: (v: number) => number, yScale: (v: number) => number): SnapResult | null;
707
+
708
+ /**
709
+ * Largest-Triangle-Three-Buckets (LTTB) downsampling algorithm.
710
+ *
711
+ * LTTB produces visually superior downsampled representations compared to
712
+ * simple min-max binning. It works by dividing data into buckets and
713
+ * selecting the point in each bucket that forms the largest triangle with
714
+ * the selected points in adjacent buckets.
715
+ *
716
+ * Reference: Sveinn Steinarsson, "Downsampling Time Series for Visual
717
+ * Representation" (2013).
718
+ *
719
+ * @module lttb
720
+ */
721
+ /** A downsampled point with its original index. */
722
+ interface LTTBPoint {
723
+ /** Pixel x coordinate. */
724
+ px: number;
725
+ /** Pixel y coordinate. */
726
+ py: number;
727
+ /** Original index in the source arrays. */
728
+ index: number;
729
+ }
730
+ /**
731
+ * Downsample data using the LTTB algorithm.
732
+ *
733
+ * @param x - Source x-values
734
+ * @param y - Source y-values
735
+ * @param startIdx - Start index (inclusive) in the source arrays
736
+ * @param endIdx - End index (exclusive) in the source arrays
737
+ * @param xScale - Function mapping data x to pixel x
738
+ * @param yScale - Function mapping data y to pixel y
739
+ * @param targetCount - Desired number of output points
740
+ * @returns Array of downsampled points
741
+ */
742
+ declare function lttbDownsample(x: Float64Array | number[], y: Float64Array | number[], startIdx: number, endIdx: number, xScale: (v: number) => number, yScale: (v: number) => number, targetCount: number): LTTBPoint[];
743
+
409
744
  /**
410
745
  * JCAMP-DX parser for spectral data.
411
746
  *
@@ -481,6 +816,32 @@ declare function parseCsvMulti(text: string, options?: Omit<CsvParseOptions, "xC
481
816
  */
482
817
  declare function parseJson(text: string): Spectrum[];
483
818
 
819
+ /**
820
+ * SPC file parser for Thermo/Galactic spectral data format.
821
+ *
822
+ * Parses the binary SPC format used by GRAMS, Thermo Scientific,
823
+ * PerkinElmer, and other spectroscopy software.
824
+ *
825
+ * Supports:
826
+ * - Single and multi-spectrum files
827
+ * - Even and uneven X spacing
828
+ * - 32-bit float and 16-bit integer Y data
829
+ * - File header metadata (resolution, instrument, etc.)
830
+ *
831
+ * Reference: "The New Galactic SPC File Format" specification
832
+ *
833
+ * @module spc
834
+ */
835
+
836
+ /**
837
+ * Parse an SPC binary file into Spectrum objects.
838
+ *
839
+ * @param buffer - ArrayBuffer containing the SPC file data
840
+ * @returns Array of parsed Spectrum objects
841
+ * @throws Error if the file is not a valid SPC file
842
+ */
843
+ declare function parseSpc(buffer: ArrayBuffer): Spectrum[];
844
+
484
845
  /**
485
846
  * Compute the x-axis extent across all visible spectra.
486
847
  */
@@ -501,4 +862,4 @@ declare function createXScale(domain: [number, number], width: number, margin: M
501
862
  */
502
863
  declare function createYScale(domain: [number, number], height: number, margin: Margin): d3_scale.ScaleLinear<number, number, never>;
503
864
 
504
- export { AxisLayer, Crosshair, type CrosshairPosition, type CrosshairProps, type CsvParseOptions, DARK_THEME, type DisplayMode, LIGHT_THEME, type Margin, type Peak, type PeakDetectionOptions, PeakMarkers, type Region, RegionSelector, type ResolvedConfig, SPECTRUM_COLORS, SpectraView, type SpectraViewProps, type Spectrum, SpectrumCanvas, type SpectrumType, type Theme, Toolbar, type UseExportReturn, type UsePeakPickingOptions, type UseSpectrumDataReturn, type UseZoomPanOptions, type UseZoomPanReturn, type ViewState, type ZoomPanState, computeXExtent, computeYExtent, createXScale, createYScale, detectPeaks, getSpectrumColor, getThemeColors, parseCsv, parseCsvMulti, parseJcamp, parseJson, useExport, usePeakPicking, useSpectrumData, useZoomPan };
865
+ export { type Annotation, AnnotationLayer, type AnnotationLayerProps, AxisLayer, Crosshair, type CrosshairPosition, type CrosshairProps, type CsvParseOptions, DARK_THEME, type DisplayMode, DropZone, type DropZoneProps, ExportMenu, type ExportMenuProps, KEYBOARD_SHORTCUTS, LIGHT_THEME, LINE_DASH_PATTERNS, type LTTBPoint, Legend, type LegendPosition$1 as LegendPosition, type LegendProps, type LineStyle, type Margin, type Peak, type PeakDetectionOptions, PeakMarkers, type Region, RegionSelector, type ResolvedConfig, SPECTRUM_COLORS, type SnapPoint, type SnapResult, SpectraView, type SpectraViewProps, type Spectrum, SpectrumCanvas, type SpectrumType, StackedView, type SvgExportOptions, type Theme, Toolbar, type UseExportReturn, type UseKeyboardNavigationOptions, type UsePeakPickingOptions, type UseRegionSelectOptions, type UseRegionSelectReturn, type UseSpectrumDataReturn, type UseZoomPanOptions, type UseZoomPanReturn, type ViewState, type ZoomPanState, binarySearchClosest, computeXExtent, computeYExtent, createXScale, createYScale, detectPeaks, downloadSvg, generateChartDescription, generateSvg, getSpectrumColor, getThemeColors, lttbDownsample, parseCsv, parseCsvMulti, parseJcamp, parseJson, parseSpc, prefersReducedMotion, snapToNearestSpectrum, useExport, useKeyboardNavigation, usePeakPicking, useRegionSelect, useResizeObserver, useSpectrumData, useZoomPan };
package/dist/index.js CHANGED
@@ -1,7 +1,16 @@
1
1
  "use client";
2
- import{useCallback as Fe,useId as st,useMemo as v,useRef as De,useState as at}from"react";import{scaleLinear as Ce}from"d3-scale";import{extent as ve}from"d3-array";var He=.05;function Z(e){let t=1/0,o=-1/0;for(let r of e){if(r.visible===!1)continue;let[n,i]=ve(r.x);n<t&&(t=n),i>o&&(o=i)}return isFinite(t)?[t,o]:[0,1]}function V(e){let t=1/0,o=-1/0;for(let i of e){if(i.visible===!1)continue;let[m,c]=ve(i.y);m<t&&(t=m),c>o&&(o=c)}if(!isFinite(t))return[0,1];let n=(o-t)*He;return[t-n,o+n]}function _(e,t,o,r){let n=t-o.left-o.right,i=r?[e[1],e[0]]:e;return Ce().domain(i).range([0,n])}function z(e,t,o){let r=t-o.top-o.bottom;return Ce().domain(e).range([r,0])}var X=["#2563eb","#dc2626","#16a34a","#9333ea","#ea580c","#0891b2","#be185d","#854d0e","#4f46e5","#65a30d"],ke={background:"#ffffff",axisColor:"#374151",gridColor:"#e5e7eb",tickColor:"#6b7280",labelColor:"#111827",crosshairColor:"#9ca3af",regionFill:"rgba(37, 99, 235, 0.1)",regionStroke:"rgba(37, 99, 235, 0.4)",tooltipBg:"#ffffff",tooltipBorder:"#d1d5db",tooltipText:"#111827"},we={background:"#111827",axisColor:"#d1d5db",gridColor:"#374151",tickColor:"#9ca3af",labelColor:"#f9fafb",crosshairColor:"#6b7280",regionFill:"rgba(96, 165, 250, 0.15)",regionStroke:"rgba(96, 165, 250, 0.5)",tooltipBg:"#1f2937",tooltipBorder:"#4b5563",tooltipText:"#f9fafb"};function j(e){return X[e%X.length]}function B(e){return e==="dark"?we:ke}import{useCallback as H,useEffect as Je,useMemo as Te,useRef as F,useState as Ye}from"react";import{zoom as We,zoomIdentity as J}from"d3-zoom";import{select as k}from"d3-selection";import"d3-transition";var Pe=1.5;function Y(e){let{plotWidth:t,plotHeight:o,xScale:r,yScale:n,scaleExtent:i=[1,50],enabled:m=!0,onViewChange:c}=e,s=F(null),u=F(null),a=F(c);a.current=c;let b=F(i);b.current=i;let[p,l]=Ye(J),f=Te(()=>p.rescaleX(r.copy()),[p,r]),d=Te(()=>p.rescaleY(n.copy()),[p,n]);Je(()=>{let y=s.current;if(!y||!m)return;let w=We().scaleExtent(b.current).extent([[0,0],[t,o]]).translateExtent([[-1/0,-1/0],[1/0,1/0]]).on("zoom",N=>{let R=N.transform;if(l(R),a.current){let L=R.rescaleX(r.copy()),O=R.rescaleY(n.copy());a.current(L.domain(),O.domain())}});return u.current=w,k(y).call(w),k(y).on("dblclick.zoom",()=>{k(y).transition().duration(300).call(w.transform,J)}),()=>{k(y).on(".zoom",null)}},[t,o,m,r,n]);let g=H(()=>{!s.current||!u.current||k(s.current).transition().duration(300).call(u.current.transform,J)},[]),h=H(()=>{!s.current||!u.current||k(s.current).transition().duration(200).call(u.current.scaleBy,Pe)},[]),S=H(()=>{!s.current||!u.current||k(s.current).transition().duration(200).call(u.current.scaleBy,1/Pe)},[]);return{zoomRef:s,state:{transform:p,isZoomed:p.k!==1||p.x!==0||p.y!==0},zoomedXScale:f,zoomedYScale:d,resetZoom:g,zoomIn:h,zoomOut:S}}import{useEffect as Re,useRef as Me}from"react";var Ge=1.5,Ke=2.5;function qe(e,t,o){e.clearRect(0,0,t,o)}function Qe(e,t,o,r,n,i){let{highlighted:m=!1,opacity:c=1}=i??{},s=Math.min(t.x.length,t.y.length);if(s<2)return;let u=t.color??j(o),a=m?Ke:Ge,[b,p]=r.domain(),l=Math.min(b,p),f=Math.max(b,p);e.save(),e.beginPath(),e.strokeStyle=u,e.lineWidth=a,e.globalAlpha=c,e.lineJoin="round";let d=!1;for(let g=0;g<s;g++){let h=t.x[g];if(h<l&&g<s-1&&t.x[g+1]<l||h>f&&g>0&&t.x[g-1]>f)continue;let S=r(h),y=n(t.y[g]);d?e.lineTo(S,y):(e.moveTo(S,y),d=!0)}e.stroke(),e.restore()}function Ee(e,t,o,r,n,i,m){qe(e,n,i),t.forEach((c,s)=>{c.visible!==!1&&Qe(e,c,s,o,r,{highlighted:c.id===m,opacity:m&&c.id!==m?.3:1})})}import{jsx as et}from"react/jsx-runtime";function W({spectra:e,xScale:t,yScale:o,width:r,height:n,highlightedId:i}){let m=Me(null),c=Me(1);return Re(()=>{let s=m.current;if(!s)return;let u=window.devicePixelRatio||1;c.current=u,s.width=r*u,s.height=n*u},[r,n]),Re(()=>{let s=m.current;if(!s)return;let u=s.getContext("2d");if(!u)return;let a=c.current;u.setTransform(a,0,0,a,0,0),Ee(u,e,t,o,r,n,i)},[e,t,o,r,n,i]),et("canvas",{ref:m,style:{width:r,height:n,position:"absolute",top:0,left:0,pointerEvents:"none"}})}import{jsx as C,jsxs as P}from"react/jsx-runtime";function Ae(e,t){let[o,r]=e.domain(),n=Math.min(o,r),m=(Math.max(o,r)-n)/(t-1);return Array.from({length:t},(c,s)=>n+s*m)}function Le(e){return Math.abs(e)>=1e3?Math.round(e).toString():Math.abs(e)>=1?e.toFixed(1):Math.abs(e)>=.01?e.toFixed(3):e.toExponential(1)}function G({xScale:e,yScale:t,width:o,height:r,xLabel:n,yLabel:i,showGrid:m=!0,colors:c}){let s=Ae(e,8),u=Ae(t,6);return P("g",{children:[m&&P("g",{children:[s.map(a=>C("line",{x1:e(a),x2:e(a),y1:0,y2:r,stroke:c.gridColor,strokeWidth:.5},`xgrid-${a}`)),u.map(a=>C("line",{x1:0,x2:o,y1:t(a),y2:t(a),stroke:c.gridColor,strokeWidth:.5},`ygrid-${a}`))]}),P("g",{transform:`translate(0, ${r})`,children:[C("line",{x1:0,x2:o,y1:0,y2:0,stroke:c.axisColor}),s.map(a=>P("g",{transform:`translate(${e(a)}, 0)`,children:[C("line",{y1:0,y2:6,stroke:c.axisColor}),C("text",{y:20,textAnchor:"middle",fill:c.tickColor,fontSize:11,fontFamily:"system-ui, sans-serif",children:Le(a)})]},`xtick-${a}`)),n&&C("text",{x:o/2,y:42,textAnchor:"middle",fill:c.labelColor,fontSize:13,fontFamily:"system-ui, sans-serif",children:n})]}),P("g",{children:[C("line",{x1:0,x2:0,y1:0,y2:r,stroke:c.axisColor}),u.map(a=>P("g",{transform:`translate(0, ${t(a)})`,children:[C("line",{x1:-6,x2:0,stroke:c.axisColor}),C("text",{x:-10,textAnchor:"end",dominantBaseline:"middle",fill:c.tickColor,fontSize:11,fontFamily:"system-ui, sans-serif",children:Le(a)})]},`ytick-${a}`)),i&&C("text",{transform:`translate(-50, ${r/2}) rotate(-90)`,textAnchor:"middle",fill:c.labelColor,fontSize:13,fontFamily:"system-ui, sans-serif",children:i})]})]})}import{jsx as K,jsxs as tt}from"react/jsx-runtime";function q({peaks:e,xScale:t,yScale:o,colors:r,onPeakClick:n}){let[i,m]=t.domain(),c=Math.min(i,m),s=Math.max(i,m),u=e.filter(a=>a.x>=c&&a.x<=s);return K("g",{className:"spectraview-peaks",children:u.map((a,b)=>{let p=t(a.x),l=o(a.y);return tt("g",{transform:`translate(${p}, ${l})`,style:{cursor:n?"pointer":"default"},onClick:()=>n?.(a),children:[K("polygon",{points:`0,-5 -5,${-5*2.5} 5,${-5*2.5}`,fill:r.labelColor,opacity:.8}),a.label&&K("text",{y:-5*2.5-14,textAnchor:"middle",fill:r.labelColor,fontSize:10,fontFamily:"system-ui, sans-serif",fontWeight:500,children:a.label})]},`peak-${a.x}-${b}`)})})}import{jsx as Q,jsxs as rt}from"react/jsx-runtime";function ee({regions:e,xScale:t,height:o,colors:r}){return Q("g",{className:"spectraview-regions",children:e.map((n,i)=>{let m=t(n.xStart),c=t(n.xEnd),s=Math.min(m,c),u=Math.abs(c-m);return rt("g",{children:[Q("rect",{x:s,y:0,width:u,height:o,fill:n.color??r.regionFill,stroke:r.regionStroke,strokeWidth:1}),n.label&&Q("text",{x:s+u/2,y:12,textAnchor:"middle",fill:r.labelColor,fontSize:10,fontFamily:"system-ui, sans-serif",children:n.label})]},`region-${i}`)})})}import{jsx as te,jsxs as re}from"react/jsx-runtime";function oe({position:e,width:t,height:o,colors:r}){return e?re("g",{className:"spectraview-crosshair",pointerEvents:"none",children:[te("line",{x1:e.px,x2:e.px,y1:0,y2:o,stroke:r.crosshairColor,strokeWidth:1,strokeDasharray:"4 4"}),te("line",{x1:0,x2:t,y1:e.py,y2:e.py,stroke:r.crosshairColor,strokeWidth:1,strokeDasharray:"4 4"}),re("g",{transform:`translate(${Math.min(e.px+10,t-100)}, ${Math.max(e.py-10,20)})`,children:[te("rect",{x:0,y:-14,width:90,height:18,rx:3,fill:r.tooltipBg,stroke:r.tooltipBorder,strokeWidth:.5,opacity:.9}),re("text",{x:5,y:0,fill:r.tooltipText,fontSize:10,fontFamily:"monospace",children:[Ie(e.dataX),", ",Ie(e.dataY)]})]})]}):null}function Ie(e){return Math.abs(e)>=100?Math.round(e).toString():Math.abs(e)>=1?e.toFixed(1):e.toFixed(4)}import{jsx as se,jsxs as nt}from"react/jsx-runtime";var ne=e=>({display:"inline-flex",alignItems:"center",justifyContent:"center",width:28,height:28,border:`1px solid ${e==="dark"?"#4b5563":"#d1d5db"}`,borderRadius:4,background:e==="dark"?"#1f2937":"#ffffff",color:e==="dark"?"#d1d5db":"#374151",fontSize:14,cursor:"pointer",padding:0,lineHeight:1}),ot=e=>({display:"flex",gap:4,padding:"4px 0",borderBottom:`1px solid ${e==="dark"?"#374151":"#e5e7eb"}`});function ae({onZoomIn:e,onZoomOut:t,onReset:o,isZoomed:r,theme:n}){return nt("div",{style:ot(n),className:"spectraview-toolbar",children:[se("button",{type:"button",style:ne(n),onClick:e,title:"Zoom in","aria-label":"Zoom in",children:"+"}),se("button",{type:"button",style:ne(n),onClick:t,title:"Zoom out","aria-label":"Zoom out",children:"\u2212"}),se("button",{type:"button",style:{...ne(n),opacity:r?1:.4},onClick:o,disabled:!r,title:"Reset zoom","aria-label":"Reset zoom",children:"\u21BA"})]})}import{jsx as x,jsxs as D}from"react/jsx-runtime";var it={top:20,right:20,bottom:50,left:65},lt=800,ct=400;function mt(e){return{width:e.width??lt,height:e.height??ct,reverseX:e.reverseX??!1,showGrid:e.showGrid??!0,showCrosshair:e.showCrosshair??!0,showToolbar:e.showToolbar??!0,displayMode:e.displayMode??"overlay",margin:{...it,...e.margin},theme:e.theme??"light"}}function ut(e,t,o){let r=e[0];return{xLabel:t??r?.xUnit??"x",yLabel:o??r?.yUnit??"y"}}function pt(e){let{spectra:t,peaks:o=[],regions:r=[],onPeakClick:n,onViewChange:i,onCrosshairMove:m}=e,s=`spectraview-clip-${st().replace(/:/g,"")}`,u=v(()=>mt(e),[e.width,e.height,e.reverseX,e.showGrid,e.showCrosshair,e.showToolbar,e.displayMode,e.margin,e.theme]),{width:a,height:b,margin:p,reverseX:l,theme:f}=u,d=a-p.left-p.right,g=b-p.top-p.bottom,h=v(()=>B(f),[f]),S=v(()=>ut(t,e.xLabel,e.yLabel),[t,e.xLabel,e.yLabel]),y=v(()=>Z(t),[t]),w=v(()=>V(t),[t]),N=v(()=>_(y,a,p,l),[y,a,p,l]),R=v(()=>z(w,b,p),[w,b,p]),L=De(i);L.current=i;let O=v(()=>(A,I)=>{L.current?.({xDomain:A,yDomain:I})},[]),{zoomRef:Oe,state:Ze,zoomedXScale:T,zoomedYScale:M,resetZoom:Ve,zoomIn:_e,zoomOut:ze}=Y({plotWidth:d,plotHeight:g,xScale:N,yScale:R,onViewChange:i?O:void 0}),[Xe,de]=at(null),be=De(m);be.current=m;let je=Fe(A=>{if(!u.showCrosshair)return;let I=A.currentTarget.getBoundingClientRect(),he=A.clientX-I.left,xe=A.clientY-I.top,ye=T.invert(he),Se=M.invert(xe);de({px:he,py:xe,dataX:ye,dataY:Se}),be.current?.(ye,Se)},[T,M,u.showCrosshair]),Be=Fe(()=>{de(null)},[]);if(t.length===0)return x("div",{style:{width:a,height:b,display:"flex",alignItems:"center",justifyContent:"center",border:`1px dashed ${h.gridColor}`,borderRadius:8,color:h.tickColor,fontFamily:"system-ui, sans-serif",fontSize:14},className:e.className,children:"No spectra loaded"});let ge=u.showToolbar?37:0;return D("div",{style:{width:a,background:h.background,borderRadius:4,overflow:"hidden"},className:e.className,children:[u.showToolbar&&x(ae,{onZoomIn:_e,onZoomOut:ze,onReset:Ve,isZoomed:Ze.isZoomed,theme:f}),D("div",{style:{position:"relative",width:a,height:b-ge},children:[x("div",{style:{position:"absolute",top:p.top,left:p.left,width:d,height:g,overflow:"hidden"},children:x(W,{spectra:t,xScale:T,yScale:M,width:d,height:g})}),x("svg",{width:a,height:b-ge,style:{position:"absolute",top:0,left:0},children:D("g",{transform:`translate(${p.left}, ${p.top})`,children:[x(G,{xScale:T,yScale:M,width:d,height:g,xLabel:S.xLabel,yLabel:S.yLabel,showGrid:u.showGrid,colors:h}),x("defs",{children:x("clipPath",{id:s,children:x("rect",{x:0,y:0,width:d,height:g})})}),D("g",{clipPath:`url(#${s})`,children:[r.length>0&&x(ee,{regions:r,xScale:T,height:g,colors:h}),o.length>0&&x(q,{peaks:o,xScale:T,yScale:M,colors:h,onPeakClick:n})]}),u.showCrosshair&&x(oe,{position:Xe,width:d,height:g,colors:h}),x("rect",{ref:Oe,x:0,y:0,width:d,height:g,fill:"transparent",style:{cursor:u.showCrosshair?"crosshair":"grab"},onMouseMove:je,onMouseLeave:Be})]})})]})]})}import{useMemo as gt}from"react";function ie(e,t,o={}){let{prominence:r=.01,minDistance:n=5,maxPeaks:i}=o;if(e.length<3||t.length<3)return[];let m=1/0,c=-1/0;for(let l=0;l<t.length;l++)t[l]<m&&(m=t[l]),t[l]>c&&(c=t[l]);let s=c-m;if(s===0)return[];let u=r*s,a=[];for(let l=1;l<t.length-1;l++)if(t[l]>t[l-1]&&t[l]>t[l+1]){let f=ft(t,l),d=dt(t,l),g=t[l]-Math.max(f,d);g>=u&&a.push({index:l,prom:g})}a.sort((l,f)=>f.prom-l.prom);let b=[];for(let l of a)b.some(d=>Math.abs(d.index-l.index)<n)||b.push(l);return(i?b.slice(0,i):b).map(l=>({x:e[l.index],y:t[l.index],label:bt(e[l.index])})).sort((l,f)=>l.x-f.x)}function ft(e,t){let o=e[t];for(let r=t-1;r>=0&&!(e[r]>e[t]);r--)e[r]<o&&(o=e[r]);return o}function dt(e,t){let o=e[t];for(let r=t+1;r<e.length&&!(e[r]>e[t]);r++)e[r]<o&&(o=e[r]);return o}function bt(e){return Math.round(e).toString()}function ht(e,t={}){let{enabled:o=!0,spectrumIds:r,prominence:n,minDistance:i,maxPeaks:m}=t;return gt(()=>{if(!o)return[];let c=r?e.filter(u=>r.includes(u.id)):e,s=[];for(let u of c){if(u.visible===!1)continue;let a=ie(u.x,u.y,{prominence:n,minDistance:i,maxPeaks:m});for(let b of a)s.push({...b,spectrumId:u.id})}return s},[e,o,r,n,i,m])}import{useCallback as E,useState as pe}from"react";var xt=[" ",",",";"," "];function Ue(e){let t=e.trim().split(/\r?\n/).slice(0,5),o=",",r=0;for(let n of xt){let i=t.map(c=>c.split(n).length-1),m=Math.min(...i);m>0&&m>=r&&(i.every(s=>s===i[0])||m>r)&&(r=m,o=n)}return o}function le(e,t={}){let{xColumn:o=0,yColumn:r=1,hasHeader:n=!0,label:i="CSV Spectrum"}=t,m=t.delimiter??Ue(e),c=e.trim().split(/\r?\n/);if(c.length<2)throw new Error("CSV file must contain at least 2 lines");let s=i,u=0;if(n){let p=c[0].split(m).map(l=>l.trim());!t.label&&p[r]&&(s=p[r]),u=1}let a=[],b=[];for(let p=u;p<c.length;p++){let l=c[p].trim();if(l===""||l.startsWith("#"))continue;let f=l.split(m),d=parseFloat(f[o]),g=parseFloat(f[r]);!isNaN(d)&&!isNaN(g)&&(a.push(d),b.push(g))}if(a.length===0)throw new Error("No valid numeric data found in CSV");return{id:`csv-${Date.now()}`,label:s,x:new Float64Array(a),y:new Float64Array(b)}}function yt(e,t={}){let{hasHeader:o=!0,label:r}=t,n=t.delimiter??Ue(e),i=e.trim().split(/\r?\n/);if(i.length<2)throw new Error("CSV file must contain at least 2 lines");let c=i[o?1:0].split(n).length;if(c<2)throw new Error("CSV must have at least 2 columns (x + y)");let s,u=0;o&&(s=i[0].split(n).map(l=>l.trim()),u=1);let a=[],b=Array.from({length:c-1},()=>[]);for(let l=u;l<i.length;l++){let f=i[l].trim();if(f===""||f.startsWith("#"))continue;let d=f.split(n),g=parseFloat(d[0]);if(!isNaN(g)){a.push(g);for(let h=1;h<c;h++){let S=parseFloat(d[h]);b[h-1].push(isNaN(S)?0:S)}}}let p=new Float64Array(a);return b.map((l,f)=>({id:`csv-${Date.now()}-${f}`,label:r??s?.[f+1]??`Spectrum ${f+1}`,x:p,y:new Float64Array(l)}))}function me(e){let t;try{t=JSON.parse(e)}catch{throw new Error("Invalid JSON: failed to parse input")}if(Array.isArray(t))return t.map((o,r)=>ce(o,r));if(typeof t=="object"&&t!==null){let o=t;return Array.isArray(o.spectra)?o.spectra.map((r,n)=>ce(r,n)):[ce(t,0)]}throw new Error("Invalid JSON structure: expected an object or array")}function ce(e,t){let o=e.x??e.wavenumbers??e.wavelengths;if(!o||!Array.isArray(o))throw new Error(`Spectrum ${t}: missing x-axis data (expected "x", "wavenumbers", or "wavelengths")`);let r=e.y??e.intensities??e.absorbance;if(!r||!Array.isArray(r))throw new Error(`Spectrum ${t}: missing y-axis data (expected "y", "intensities", or "absorbance")`);if(o.length!==r.length)throw new Error(`Spectrum ${t}: x and y arrays must have the same length (got ${o.length} and ${r.length})`);let n=e.label??e.title??e.name??`Spectrum ${t+1}`;return{id:`json-${Date.now()}-${t}`,label:n,x:new Float64Array(o),y:new Float64Array(r),xUnit:e.xUnit,yUnit:e.yUnit,type:e.type,meta:e.meta}}var U=null,$e=!1;async function St(){if($e)return U;$e=!0;try{U=await import("jcampconverter")}catch{U=null}return U}function Ne(e){let t=(e["DATA TYPE"]??e.DATATYPE??"").toLowerCase();return t.includes("infrared")||t.includes("ir")?"IR":t.includes("raman")?"Raman":t.includes("nir")||t.includes("near")?"NIR":t.includes("uv")||t.includes("vis")?"UV-Vis":t.includes("fluor")?"fluorescence":"other"}async function ue(e){let t=await St();return t?Ct(e,t):[vt(e)]}function Ct(e,t){return t.convert(e,{keepRecordsRegExp:/.*/}).flatten.map((r,n)=>{let i=r.spectra?.[0]?.data?.[0];if(!i)throw new Error(`JCAMP block ${n}: no spectral data found`);return{id:`jcamp-${Date.now()}-${n}`,label:r.info?.TITLE??`Spectrum ${n+1}`,x:new Float64Array(i.x),y:new Float64Array(i.y),xUnit:r.info?.XUNITS??"cm\u207B\xB9",yUnit:r.info?.YUNITS??"Absorbance",type:Ne(r.info),meta:r.info}})}function vt(e){let t=e.split(/\r?\n/),o={},r=[],n=[],i=!1;for(let m of t){let c=m.trim();if(c.startsWith("##")){let s=c.match(/^##(.+?)=\s*(.*)$/);if(s){let u=s[1].trim().toUpperCase(),a=s[2].trim();if(u==="XYDATA"||u==="XYPOINTS"){i=!0;continue}if(u==="END"){i=!1;continue}o[u]=a}continue}if(i&&c!==""){let s=c.split(/[\s,]+/).map(Number);if(s.length>=2&&!s.some(isNaN)){let u=s[0],a=parseFloat(o.FIRSTX??"0"),b=parseFloat(o.LASTX??"0"),p=parseInt(o.NPOINTS??"0",10);if(p>0&&s.length===2)r.push(s[0]),n.push(s[1]);else if(s.length>1){let l=p>1?(b-a)/(p-1):0;for(let f=1;f<s.length;f++)r.push(u+(f-1)*l),n.push(s[f])}}}}if(r.length===0)throw new Error("Failed to parse JCAMP-DX: no data found. Install jcampconverter for full format support.");return{id:`jcamp-${Date.now()}`,label:o.TITLE??"JCAMP Spectrum",x:new Float64Array(r),y:new Float64Array(n),xUnit:o.XUNITS??"cm\u207B\xB9",yUnit:o.YUNITS??"Absorbance",type:Ne(o),meta:o}}function kt(e){switch(e.toLowerCase().split(".").pop()){case"dx":case"jdx":case"jcamp":return"jcamp";case"csv":case"tsv":case"txt":return"csv";case"json":return"json";default:return null}}function wt(e=[]){let[t,o]=pe(e),[r,n]=pe(!1),[i,m]=pe(null),c=E(async(l,f)=>{n(!0),m(null);try{let d;switch(f){case"jcamp":d=await ue(l);break;case"csv":d=[le(l)];break;case"json":d=me(l);break}o(g=>[...g,...d])}catch(d){let g=d instanceof Error?d.message:"Failed to parse file";m(g)}finally{n(!1)}},[]),s=E(async l=>{let f=kt(l.name);if(!f){m(`Unsupported file format: ${l.name}`);return}let d=await l.text();await c(d,f)},[c]),u=E(l=>{o(f=>[...f,l])},[]),a=E(l=>{o(f=>f.filter(d=>d.id!==l))},[]),b=E(l=>{o(f=>f.map(d=>d.id===l?{...d,visible:d.visible===!1}:d))},[]),p=E(()=>{o([]),m(null)},[]);return{spectra:t,loading:r,error:i,loadFile:s,loadText:c,addSpectrum:u,removeSpectrum:a,toggleVisibility:b,clear:p}}import{useCallback as fe}from"react";function $(e,t){let o=URL.createObjectURL(e),r=document.createElement("a");r.href=o,r.download=t,document.body.appendChild(r),r.click(),document.body.removeChild(r),URL.revokeObjectURL(o)}function Tt(){let e=fe((r,n="spectrum.png")=>{r.toBlob(i=>{i&&$(i,n)},"image/png")},[]),t=fe((r,n="spectra.csv")=>{let i=r.filter(m=>m.visible!==!1);if(i.length!==0)if(i.length===1){let m=i[0],c=`${m.xUnit??"x"},${m.yUnit??"y"}
3
- `,s=Array.from(m.x).map((a,b)=>`${a},${m.y[b]}`),u=c+s.join(`
4
- `);$(new Blob([u],{type:"text/csv"}),n)}else{let m=Math.max(...i.map(a=>a.x.length)),c=i.map(a=>`${a.label}_x,${a.label}_y`).join(","),s=[];for(let a=0;a<m;a++){let b=i.map(p=>a<p.x.length?`${p.x[a]},${p.y[a]}`:",");s.push(b.join(","))}let u=c+`
5
- `+s.join(`
6
- `);$(new Blob([u],{type:"text/csv"}),n)}},[]),o=fe((r,n="spectra.json")=>{let m=r.filter(s=>s.visible!==!1).map(s=>({label:s.label,x:Array.from(s.x),y:Array.from(s.y),xUnit:s.xUnit,yUnit:s.yUnit,type:s.type})),c=JSON.stringify(m,null,2);$(new Blob([c],{type:"application/json"}),n)},[]);return{exportPng:e,exportCsv:t,exportJson:o}}export{G as AxisLayer,oe as Crosshair,we as DARK_THEME,ke as LIGHT_THEME,q as PeakMarkers,ee as RegionSelector,X as SPECTRUM_COLORS,pt as SpectraView,W as SpectrumCanvas,ae as Toolbar,Z as computeXExtent,V as computeYExtent,_ as createXScale,z as createYScale,ie as detectPeaks,j as getSpectrumColor,B as getThemeColors,le as parseCsv,yt as parseCsvMulti,ue as parseJcamp,me as parseJson,Tt as useExport,ht as usePeakPicking,wt as useSpectrumData,Y as useZoomPan};
2
+ import{useCallback as vt,useId as wr,useMemo as U,useRef as Ct,useState as Ze}from"react";import{scaleLinear as st}from"d3-scale";import{extent as at}from"d3-array";var Zt=.05;function be(e){let t=1/0,r=-1/0;for(let o of e){if(o.visible===!1)continue;let[n,i]=at(o.x);n<t&&(t=n),i>r&&(r=i)}return isFinite(t)?[t,r]:[0,1]}function B(e){let t=1/0,r=-1/0;for(let i of e){if(i.visible===!1)continue;let[l,s]=at(i.y);l<t&&(t=l),s>r&&(r=s)}if(!isFinite(t))return[0,1];let n=(r-t)*Zt;return[t-n,r+n]}function ge(e,t,r,o){let n=t-r.left-r.right,i=o?[e[1],e[0]]:e;return st().domain(i).range([0,n])}function X(e,t,r){let o=t-r.top-r.bottom;return st().domain(e).range([o,0])}var he=["#2563eb","#dc2626","#16a34a","#9333ea","#ea580c","#0891b2","#be185d","#854d0e","#4f46e5","#65a30d"],lt={background:"#ffffff",axisColor:"#374151",gridColor:"#e5e7eb",tickColor:"#6b7280",labelColor:"#111827",crosshairColor:"#9ca3af",regionFill:"rgba(37, 99, 235, 0.1)",regionStroke:"rgba(37, 99, 235, 0.4)",tooltipBg:"#ffffff",tooltipBorder:"#d1d5db",tooltipText:"#111827"},ct={background:"#111827",axisColor:"#d1d5db",gridColor:"#374151",tickColor:"#9ca3af",labelColor:"#f9fafb",crosshairColor:"#6b7280",regionFill:"rgba(96, 165, 250, 0.15)",regionStroke:"rgba(96, 165, 250, 0.5)",tooltipBg:"#1f2937",tooltipBorder:"#4b5563",tooltipText:"#f9fafb"};function A(e){return he[e%he.length]}function H(e){return e==="dark"?ct:lt}import{useCallback as ye,useEffect as _t,useMemo as mt,useRef as te,useState as Bt}from"react";import{zoom as Xt,zoomIdentity as xe}from"d3-zoom";import{select as z}from"d3-selection";import"d3-transition";var ut=1.5;function Se(e){let{plotWidth:t,plotHeight:r,xScale:o,yScale:n,scaleExtent:i=[1,50],enabled:l=!0,onViewChange:s}=e,c=te(null),m=te(null),u=te(s);u.current=s;let p=te(i);p.current=i;let[f,a]=Bt(xe),d=mt(()=>f.rescaleX(o.copy()),[f,o]),g=mt(()=>f.rescaleY(n.copy()),[f,n]);_t(()=>{let v=c.current;if(!v||!l)return;let k=Xt().scaleExtent(p.current).extent([[0,0],[t,r]]).translateExtent([[-1/0,-1/0],[1/0,1/0]]).on("zoom",h=>{let y=h.transform;if(a(y),u.current){let w=y.rescaleX(o.copy()),x=y.rescaleY(n.copy());u.current(w.domain(),x.domain())}});return m.current=k,z(v).call(k),z(v).on("dblclick.zoom",()=>{z(v).transition().duration(300).call(k.transform,xe)}),()=>{z(v).on(".zoom",null)}},[t,r,l,o,n]);let S=ye(()=>{!c.current||!m.current||z(c.current).transition().duration(300).call(m.current.transform,xe)},[]),b=ye(()=>{!c.current||!m.current||z(c.current).transition().duration(200).call(m.current.scaleBy,ut)},[]),C=ye(()=>{!c.current||!m.current||z(c.current).transition().duration(200).call(m.current.scaleBy,1/ut)},[]);return{zoomRef:c,state:{transform:f,isZoomed:f.k!==1||f.x!==0||f.y!==0},zoomedXScale:d,zoomedYScale:g,resetZoom:S,zoomIn:b,zoomOut:C}}import{forwardRef as Gt,useEffect as ft,useImperativeHandle as Jt,useRef as dt}from"react";function ve(e,t,r,o,n,i,l){let s=o-r;if(s<=l){let f=[];for(let a=r;a<o;a++)f.push({px:n(e[a]),py:i(t[a]),index:a});return f}let c=[];c.push({px:n(e[r]),py:i(t[r]),index:r});let m=l-2,u=(s-2)/m,p=r;for(let f=0;f<m;f++){let a=r+1+Math.floor(f*u),d=r+1+Math.min(Math.floor((f+1)*u),s-2),g=d,S=r+1+Math.min(Math.floor((f+2)*u),s-2),b,C;if(f===m-1)b=n(e[o-1]),C=i(t[o-1]);else{b=0,C=0;let w=S-g;for(let x=g;x<S;x++)b+=n(e[x]),C+=i(t[x]);w>0&&(b/=w,C/=w)}let v=n(e[p]),k=i(t[p]),h=-1,y=a;for(let w=a;w<d;w++){let x=n(e[w]),R=i(t[w]),E=Math.abs((v-b)*(R-k)-(v-x)*(C-k));E>h&&(h=E,y=w)}c.push({px:n(e[y]),py:i(t[y]),index:y}),p=y}return c.push({px:n(e[o-1]),py:i(t[o-1]),index:o-1}),c}var Ht=1.5,Wt={solid:[],dashed:[8,4],dotted:[2,2],"dash-dot":[8,4,2,4]},Yt=2e3;function jt(e,t,r){e.clearRect(0,0,t,r)}function Kt(e,t,r,o,n,i,l){let{highlighted:s=!1,opacity:c=1}=l??{},m=Math.min(t.x.length,t.y.length);if(m<2)return;let u=t.color??A(r),p=t.lineWidth??Ht,f=s?p+1:p,a=Wt[t.lineStyle??"solid"]??[],[d,g]=o.domain(),S=Math.min(d,g),b=Math.max(d,g),C=0,v=m;for(let h=0;h<m;h++)if(t.x[h]>=S||h<m-1&&t.x[h+1]>=S){C=Math.max(0,h-1);break}for(let h=m-1;h>=0;h--)if(t.x[h]<=b||h>0&&t.x[h-1]<=b){v=Math.min(m,h+2);break}let k=v-C;if(e.save(),e.beginPath(),e.strokeStyle=u,e.lineWidth=f,e.globalAlpha=c,e.lineJoin="round",e.setLineDash(a),k>Yt){let h=Math.max(Math.ceil(i*2),200),y=ve(t.x,t.y,C,v,o,n,h);if(y.length>0){e.moveTo(y[0].px,y[0].py);for(let w=1;w<y.length;w++)e.lineTo(y[w].px,y[w].py)}}else{let h=!1;for(let y=C;y<v;y++){let w=o(t.x[y]),x=n(t.y[y]);h?e.lineTo(w,x):(e.moveTo(w,x),h=!0)}}e.stroke(),e.restore()}function pt(e,t,r,o,n,i,l){jt(e,n,i),t.forEach((s,c)=>{s.visible!==!1&&Kt(e,s,c,r,o,n,{highlighted:s.id===l,opacity:l&&s.id!==l?.3:1})})}import{jsx as qt}from"react/jsx-runtime";var W=Gt(function({spectra:t,xScale:r,yScale:o,width:n,height:i,highlightedId:l},s){let c=dt(null),m=dt(1);return Jt(s,()=>c.current,[]),ft(()=>{let u=c.current;if(!u)return;let p=window.devicePixelRatio||1;m.current=p,u.width=n*p,u.height=i*p},[n,i]),ft(()=>{let u=c.current;if(!u)return;let p=u.getContext("2d");if(!p)return;let f=m.current;p.setTransform(f,0,0,f,0,0),pt(p,t,r,o,n,i,l)},[t,r,o,n,i,l]),qt("canvas",{ref:c,style:{width:n,height:i,position:"absolute",top:0,left:0,pointerEvents:"none"}})});import{jsx as D,jsxs as V}from"react/jsx-runtime";function bt(e,t){let[r,o]=e.domain(),n=Math.min(r,o),l=(Math.max(r,o)-n)/(t-1);return Array.from({length:t},(s,c)=>n+c*l)}function gt(e){return Math.abs(e)>=1e3?Math.round(e).toString():Math.abs(e)>=1?e.toFixed(1):Math.abs(e)>=.01?e.toFixed(3):e.toExponential(1)}function Y({xScale:e,yScale:t,width:r,height:o,xLabel:n,yLabel:i,showGrid:l=!0,colors:s}){let c=bt(e,8),m=bt(t,6);return V("g",{children:[l&&V("g",{children:[c.map(u=>D("line",{x1:e(u),x2:e(u),y1:0,y2:o,stroke:s.gridColor,strokeWidth:.5},`xgrid-${u}`)),m.map(u=>D("line",{x1:0,x2:r,y1:t(u),y2:t(u),stroke:s.gridColor,strokeWidth:.5},`ygrid-${u}`))]}),V("g",{transform:`translate(0, ${o})`,children:[D("line",{x1:0,x2:r,y1:0,y2:0,stroke:s.axisColor}),c.map(u=>V("g",{transform:`translate(${e(u)}, 0)`,children:[D("line",{y1:0,y2:6,stroke:s.axisColor}),D("text",{y:20,textAnchor:"middle",fill:s.tickColor,fontSize:11,fontFamily:"system-ui, sans-serif",children:gt(u)})]},`xtick-${u}`)),n&&D("text",{x:r/2,y:42,textAnchor:"middle",fill:s.labelColor,fontSize:13,fontFamily:"system-ui, sans-serif",children:n})]}),V("g",{children:[D("line",{x1:0,x2:0,y1:0,y2:o,stroke:s.axisColor}),m.map(u=>V("g",{transform:`translate(0, ${t(u)})`,children:[D("line",{x1:-6,x2:0,stroke:s.axisColor}),D("text",{x:-10,textAnchor:"end",dominantBaseline:"middle",fill:s.tickColor,fontSize:11,fontFamily:"system-ui, sans-serif",children:gt(u)})]},`ytick-${u}`)),i&&D("text",{transform:`translate(-50, ${o/2}) rotate(-90)`,textAnchor:"middle",fill:s.labelColor,fontSize:13,fontFamily:"system-ui, sans-serif",children:i})]})]})}import{jsx as Ce,jsxs as Qt}from"react/jsx-runtime";function we({peaks:e,xScale:t,yScale:r,colors:o,onPeakClick:n}){let[i,l]=t.domain(),s=Math.min(i,l),c=Math.max(i,l),m=e.filter(u=>u.x>=s&&u.x<=c);return Ce("g",{className:"spectraview-peaks",children:m.map((u,p)=>{let f=t(u.x),a=r(u.y);return Qt("g",{transform:`translate(${f}, ${a})`,style:{cursor:n?"pointer":"default"},onClick:()=>n?.(u),children:[Ce("polygon",{points:`0,-5 -5,${-5*2.5} 5,${-5*2.5}`,fill:o.labelColor,opacity:.8}),u.label&&Ce("text",{y:-5*2.5-14,textAnchor:"middle",fill:o.labelColor,fontSize:10,fontFamily:"system-ui, sans-serif",fontWeight:500,children:u.label})]},`peak-${u.x}-${p}`)})})}import{jsx as ke,jsxs as er}from"react/jsx-runtime";function Re({regions:e,xScale:t,height:r,colors:o}){return ke("g",{className:"spectraview-regions",children:e.map((n,i)=>{let l=t(n.xStart),s=t(n.xEnd),c=Math.min(l,s),m=Math.abs(s-l);return er("g",{children:[ke("rect",{x:c,y:0,width:m,height:r,fill:n.color??o.regionFill,stroke:o.regionStroke,strokeWidth:1}),n.label&&ke("text",{x:c+m/2,y:12,textAnchor:"middle",fill:o.labelColor,fontSize:10,fontFamily:"system-ui, sans-serif",children:n.label})]},`region-${i}`)})})}import{jsx as re,jsxs as Te}from"react/jsx-runtime";function Ee({position:e,width:t,height:r,colors:o,snapPoint:n}){return e?Te("g",{className:"spectraview-crosshair",pointerEvents:"none",children:[re("line",{x1:e.px,x2:e.px,y1:0,y2:r,stroke:o.crosshairColor,strokeWidth:1,strokeDasharray:"4 4"}),re("line",{x1:0,x2:t,y1:e.py,y2:e.py,stroke:o.crosshairColor,strokeWidth:1,strokeDasharray:"4 4"}),n&&re("circle",{cx:n.px,cy:n.py,r:4,fill:n.color??o.crosshairColor,stroke:o.background,strokeWidth:1.5}),Te("g",{transform:`translate(${Math.min(e.px+10,t-100)}, ${Math.max(e.py-10,20)})`,children:[re("rect",{x:0,y:-14,width:90,height:18,rx:3,fill:o.tooltipBg,stroke:o.tooltipBorder,strokeWidth:.5,opacity:.9}),Te("text",{x:5,y:0,fill:o.tooltipText,fontSize:10,fontFamily:"monospace",children:[ht(n?.dataX??e.dataX),","," ",ht(n?.dataY??e.dataY)]})]})]}):null}function ht(e){return Math.abs(e)>=100?Math.round(e).toString():Math.abs(e)>=1?e.toFixed(1):e.toFixed(4)}import{jsx as j,jsxs as tr}from"react/jsx-runtime";function Me({annotations:e,xScale:t,yScale:r,colors:o}){return e.length===0?null:j("g",{className:"spectraview-annotations",pointerEvents:"none",children:e.map(n=>{let i=t(n.x),l=r(n.y),[s,c]=n.offset??[0,-20],m=i+s,u=l+c,p=n.fontSize??11,f=n.color??o.tickColor,a=n.showAnchorLine!==!1;return tr("g",{children:[a&&j("line",{x1:i,y1:l,x2:m,y2:u,stroke:f,strokeWidth:.75,strokeDasharray:"3 2",opacity:.6}),j("circle",{cx:i,cy:l,r:2.5,fill:f,opacity:.8}),j("text",{x:m,y:u,fill:o.background,fontSize:p,fontFamily:"system-ui, sans-serif",textAnchor:"middle",dominantBaseline:"auto",stroke:o.background,strokeWidth:3,strokeLinejoin:"round",children:n.text}),j("text",{x:m,y:u,fill:f,fontSize:p,fontFamily:"system-ui, sans-serif",textAnchor:"middle",dominantBaseline:"auto",children:n.text})]},n.id)})})}function yt(e,t,r){if(r===0)return-1;if(r===1)return 0;let o=e[r-1]>=e[0],n=0,i=r-1;for(;n<i-1;){let c=n+i>>>1,m=e[c];o?m<=t?n=c:i=c:m>=t?n=c:i=c}let l=Math.abs(e[n]-t),s=Math.abs(e[i]-t);return l<=s?n:i}function Le(e,t,r,o,n){let i=null;for(let l of e){if(l.visible===!1)continue;let s=Math.min(l.x.length,l.y.length);if(s<2)continue;let c=yt(l.x,t,s);if(c<0)continue;let m=l.x[c],u=l.y[c],p=Math.abs(o(m)-o(t)),f=Math.abs(n(u)-r),a=Math.sqrt(p*p+f*f);(!i||a<i.distance)&&(i={spectrumId:l.id,index:c,x:m,y:u,distance:a})}return i}import{memo as rr}from"react";import{jsx as Ae,jsxs as nr}from"react/jsx-runtime";var Pe=e=>({display:"inline-flex",alignItems:"center",justifyContent:"center",width:28,height:28,border:`1px solid ${e==="dark"?"#4b5563":"#d1d5db"}`,borderRadius:4,background:e==="dark"?"#1f2937":"#ffffff",color:e==="dark"?"#d1d5db":"#374151",fontSize:14,cursor:"pointer",padding:0,lineHeight:1}),or=e=>({display:"flex",gap:4,padding:"4px 0",borderBottom:`1px solid ${e==="dark"?"#374151":"#e5e7eb"}`}),De=rr(function({onZoomIn:t,onZoomOut:r,onReset:o,isZoomed:n,theme:i}){return nr("div",{style:or(i),className:"spectraview-toolbar",children:[Ae("button",{type:"button",style:Pe(i),onClick:t,title:"Zoom in","aria-label":"Zoom in",children:"+"}),Ae("button",{type:"button",style:Pe(i),onClick:r,title:"Zoom out","aria-label":"Zoom out",children:"\u2212"}),Ae("button",{type:"button",style:{...Pe(i),opacity:n?1:.4},onClick:o,disabled:!n,title:"Reset zoom","aria-label":"Reset zoom",children:"\u21BA"})]})});import{memo as ir}from"react";import{jsx as Ie,jsxs as cr}from"react/jsx-runtime";var sr=(e,t)=>({display:"flex",flexDirection:t==="left"||t==="right"?"column":"row",flexWrap:"wrap",gap:6,padding:"4px 8px",fontSize:12,fontFamily:"system-ui, sans-serif",borderTop:t==="bottom"?`1px solid ${e==="dark"?"#374151":"#e5e7eb"}`:void 0,borderBottom:t==="top"?`1px solid ${e==="dark"?"#374151":"#e5e7eb"}`:void 0,borderLeft:t==="right"?`1px solid ${e==="dark"?"#374151":"#e5e7eb"}`:void 0,borderRight:t==="left"?`1px solid ${e==="dark"?"#374151":"#e5e7eb"}`:void 0}),ar=(e,t,r)=>({display:"inline-flex",alignItems:"center",gap:4,cursor:"pointer",opacity:t?.4:1,fontWeight:r?600:400,color:e==="dark"?"#e5e7eb":"#374151",userSelect:"none",padding:"2px 4px",borderRadius:3,background:r?e==="dark"?"rgba(255,255,255,0.08)":"rgba(0,0,0,0.04)":"transparent",transition:"background 0.15s, opacity 0.15s"}),lr=(e,t)=>({width:12,height:3,borderRadius:1,background:e,opacity:t?.4:1,flexShrink:0}),oe=ir(function({spectra:t,theme:r,position:o,onToggleVisibility:n,onHighlight:i,highlightedId:l}){return t.length<=1?null:Ie("div",{className:"spectraview-legend",style:sr(r,o),role:"list","aria-label":"Spectrum legend",children:t.map((s,c)=>{let m=s.color??A(c),u=s.visible===!1,p=l===s.id;return cr("div",{role:"listitem",style:ar(r,u,p),onClick:()=>n?.(s.id),onMouseEnter:()=>i?.(s.id),onMouseLeave:()=>i?.(null),title:u?`Show ${s.label}`:`Hide ${s.label}`,children:[Ie("span",{style:lr(m,u)}),Ie("span",{style:{textDecoration:u?"line-through":"none",maxWidth:120,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:s.label})]},s.id)})})});import{useCallback as ne,useState as mr}from"react";import{jsx as ur,jsxs as pr}from"react/jsx-runtime";function Ue({enabled:e,theme:t,width:r,height:o,onDrop:n,children:i}){let[l,s]=mr(!1),c={current:0},m=ne(a=>{e&&(a.preventDefault(),c.current++,s(!0))},[e]),u=ne(a=>{e&&(a.preventDefault(),c.current--,c.current<=0&&(c.current=0,s(!1)))},[e]),p=ne(a=>{e&&(a.preventDefault(),a.dataTransfer.dropEffect="copy")},[e]),f=ne(a=>{if(!e)return;a.preventDefault(),c.current=0,s(!1);let d=Array.from(a.dataTransfer.files);d.length>0&&n?.(d)},[e,n]);return pr("div",{style:{position:"relative",width:r,height:o},onDragEnter:m,onDragLeave:u,onDragOver:p,onDrop:f,children:[i,l&&ur("div",{"data-testid":"dropzone-overlay",style:{position:"absolute",inset:0,display:"flex",alignItems:"center",justifyContent:"center",background:t==="dark"?"rgba(30, 58, 138, 0.6)":"rgba(59, 130, 246, 0.15)",border:`2px dashed ${t==="dark"?"#60a5fa":"#3b82f6"}`,borderRadius:4,zIndex:100,pointerEvents:"none",fontSize:14,fontFamily:"system-ui, sans-serif",color:t==="dark"?"#93c5fd":"#1d4ed8",fontWeight:500},children:"Drop spectrum files here"})]})}import{useMemo as fr}from"react";import{jsx as Z,jsxs as dr}from"react/jsx-runtime";var xt=8;function Fe({spectra:e,xScale:t,plotWidth:r,plotHeight:o,margin:n,theme:i,showGrid:l,xLabel:s,yLabel:c}){let m=e.filter(d=>d.visible!==!1),u=fr(()=>H(i),[i]),p=m.length,f=(p-1)*xt,a=Math.max(40,Math.floor((o-f)/Math.max(p,1)));return Z("g",{className:"spectraview-stacked",children:m.map((d,g)=>{let S=g*(a+xt),b=B([d]),C=X(b,a+n.top+n.bottom,{...n,top:0,bottom:0}),v=d.color??A(g),k={...d,color:v};return dr("g",{transform:`translate(0, ${S})`,children:[Z("rect",{x:0,y:0,width:r,height:a,fill:"transparent",stroke:u.gridColor,strokeWidth:.5,rx:2}),Z(Y,{xScale:t,yScale:C,width:r,height:a,xLabel:g===p-1?s:"",yLabel:c,showGrid:l,colors:u}),Z("text",{x:4,y:14,fill:v,fontSize:11,fontFamily:"system-ui, sans-serif",fontWeight:500,children:d.label}),Z("foreignObject",{x:0,y:0,width:r,height:a,children:Z(W,{spectra:[k],xScale:t,yScale:C,width:r,height:a})})]},d.id)})})}import{useCallback as $e,useRef as br,useState as gr}from"react";function Oe(e){let{enabled:t,xScale:r,onRegionSelect:o}=e,[n,i]=gr(null),l=br(null),s=$e(u=>{if(!t||!u.shiftKey)return;u.preventDefault();let p=u.currentTarget.getBoundingClientRect(),f=u.clientX-p.left,a=r.invert(f);l.current=a,i({xStart:a,xEnd:a})},[t,r]),c=$e(u=>{if(l.current===null)return;let p=u.currentTarget.getBoundingClientRect(),f=u.clientX-p.left,a=r.invert(f),d=l.current;i({xStart:Math.min(d,a),xEnd:Math.max(d,a)})},[r]),m=$e(()=>{if(l.current===null||!n)return;Math.abs(n.xEnd-n.xStart)>0&&o?.(n),l.current=null,i(null)},[n,o]);return{pendingRegion:n,handleMouseDown:s,handleMouseMove:c,handleMouseUp:m}}import{useCallback as hr,useEffect as yr,useRef as St,useState as xr}from"react";function Ne(){let[e,t]=xr(null),r=St(null),o=St(null),n=hr(i=>{if(r.current&&(r.current.disconnect(),r.current=null),o.current=i,!i)return;let l=new ResizeObserver(m=>{let u=m[0];if(!u)return;let{width:p,height:f}=u.contentRect;t({width:Math.round(p),height:Math.round(f)})});l.observe(i),r.current=l;let{width:s,height:c}=i.getBoundingClientRect();t({width:Math.round(s),height:Math.round(c)})},[]);return yr(()=>()=>{r.current?.disconnect()},[]),{ref:n,size:e}}import{useCallback as Sr}from"react";function ze(e){let{onZoomIn:t,onZoomOut:r,onReset:o,enabled:n=!0}=e;return Sr(l=>{if(n)switch(l.key){case"+":case"=":l.preventDefault(),t();break;case"-":l.preventDefault(),r();break;case"Escape":l.preventDefault(),o();break}},[n,t,r,o])}function vr(){return typeof window>"u"?!1:window.matchMedia("(prefers-reduced-motion: reduce)").matches}function Ve(e,t,r){return e===0?"Empty spectrum viewer":`Interactive spectrum viewer showing ${e} ${e===1?"spectrum":"spectra"}. X-axis: ${t}. Y-axis: ${r}. Use arrow keys to pan, +/- to zoom, Escape to reset.`}var Cr={PAN_LEFT:"ArrowLeft",PAN_RIGHT:"ArrowRight",PAN_UP:"ArrowUp",PAN_DOWN:"ArrowDown",ZOOM_IN:"+",ZOOM_IN_ALT:"=",ZOOM_OUT:"-",RESET:"Escape",NEXT_PEAK:"Tab",PREV_PEAK:"Shift+Tab"};import{Fragment as Pr,jsx as T,jsxs as K}from"react/jsx-runtime";var kr={top:20,right:20,bottom:50,left:65},Rr=800,Tr=400;function Er(e){return{width:e.width??Rr,height:e.height??Tr,reverseX:e.reverseX??!1,showGrid:e.showGrid??!0,showCrosshair:e.showCrosshair??!0,showToolbar:e.showToolbar??!0,showLegend:e.showLegend??!0,legendPosition:e.legendPosition??"bottom",displayMode:e.displayMode??"overlay",margin:{...kr,...e.margin},theme:e.theme??"light",responsive:e.responsive??!1,enableDragDrop:e.enableDragDrop??!1,enableRegionSelect:e.enableRegionSelect??!1}}function Mr(e,t,r){let o=e[0];return{xLabel:t??o?.xUnit??"x",yLabel:r??o?.yUnit??"y"}}function Lr(e){let{spectra:t,peaks:r=[],regions:o=[],annotations:n=[],onPeakClick:i,onViewChange:l,onCrosshairMove:s,onToggleVisibility:c,onFileDrop:m,onRegionSelect:u,canvasRef:p,snapCrosshair:f=!0}=e,{ref:a,size:d}=Ne(),S=`spectraview-clip-${wr().replace(/:/g,"")}`,b=U(()=>Er(e),[e.width,e.height,e.reverseX,e.showGrid,e.showCrosshair,e.showToolbar,e.showLegend,e.legendPosition,e.displayMode,e.margin,e.theme,e.responsive,e.enableDragDrop,e.enableRegionSelect]),C=b.responsive&&d?d.width:b.width,{height:v,margin:k,reverseX:h,theme:y}=b,w=C-k.left-k.right,x=v-k.top-k.bottom,R=U(()=>H(y),[y]),E=U(()=>Mr(t,e.xLabel,e.yLabel),[t,e.xLabel,e.yLabel]),L=U(()=>be(t),[t]),P=U(()=>B(t),[t]),ce=U(()=>ge(L,C,k,h),[L,C,k,h]),O=U(()=>X(P,v,k),[P,v,k]),N=Ct(l);N.current=l;let Pt=U(()=>($,Q)=>{N.current?.({xDomain:$,yDomain:Q})},[]),{zoomRef:Ge,state:At,zoomedXScale:M,zoomedYScale:F,resetZoom:Je,zoomIn:qe,zoomOut:Qe}=Se({plotWidth:w,plotHeight:x,xScale:ce,yScale:O,onViewChange:l?Pt:void 0}),{pendingRegion:J,handleMouseDown:Dt,handleMouseMove:It,handleMouseUp:Ut}=Oe({enabled:b.enableRegionSelect,xScale:M,onRegionSelect:u}),[me,et]=Ze(null),[Ft,tt]=Ze(null),[$t,ue]=Ze(null),q=Ct(s);q.current=s;let rt=vt($=>{if(!b.showCrosshair)return;let Q=$.currentTarget.getBoundingClientRect(),nt=$.clientX-Q.left,fe=$.clientY-Q.top,ee=M.invert(nt),de=F.invert(fe);if(tt({px:nt,py:fe,dataX:ee,dataY:de}),f&&t.length>0){let I=Le(t,ee,fe,M,F);if(I&&I.distance<50){let it=t.findIndex(Vt=>Vt.id===I.spectrumId);ue({px:M(I.x),py:F(I.y),dataX:I.x,dataY:I.y,color:t[it]?.color??A(it)}),q.current?.(I.x,I.y)}else ue(null),q.current?.(ee,de)}else q.current?.(ee,de)},[M,F,b.showCrosshair,f,t]),ot=vt(()=>{tt(null),ue(null)},[]),Ot=ze({onZoomIn:qe,onZoomOut:Qe,onReset:Je}),Nt=U(()=>Ve(t.length,E.xLabel,E.yLabel),[t.length,E.xLabel,E.yLabel]),zt=b.displayMode==="stacked";if(t.length===0)return T("div",{ref:b.responsive?a:void 0,style:{width:b.responsive?"100%":C,height:v,display:"flex",alignItems:"center",justifyContent:"center",border:`1px dashed ${R.gridColor}`,borderRadius:8,color:R.tickColor,fontFamily:"system-ui, sans-serif",fontSize:14},className:e.className,children:"No spectra loaded"});let pe=b.showToolbar?37:0;return K("div",{ref:b.responsive?a:void 0,style:{width:b.responsive?"100%":C,background:R.background,borderRadius:4,overflow:"hidden"},className:e.className,role:"img","aria-label":Nt,tabIndex:0,onKeyDown:Ot,children:[b.showToolbar&&T(De,{onZoomIn:qe,onZoomOut:Qe,onReset:Je,isZoomed:At.isZoomed,theme:y}),b.showLegend&&b.legendPosition==="top"&&T(oe,{spectra:t,theme:y,position:"top",onToggleVisibility:c,onHighlight:et,highlightedId:me}),T(Ue,{enabled:b.enableDragDrop,theme:y,width:C,height:v-pe,onDrop:m,children:zt?T("svg",{width:C,height:v-pe,style:{position:"absolute",top:0,left:0},children:K("g",{transform:`translate(${k.left}, ${k.top})`,children:[T(Fe,{spectra:t,xScale:M,plotWidth:w,plotHeight:x,margin:k,theme:y,showGrid:b.showGrid,xLabel:E.xLabel,yLabel:E.yLabel}),T("rect",{ref:Ge,x:0,y:0,width:w,height:x,fill:"transparent",style:{cursor:"grab"},onMouseMove:rt,onMouseLeave:ot})]})}):K(Pr,{children:[T("div",{style:{position:"absolute",top:k.top,left:k.left,width:w,height:x,overflow:"hidden"},children:T(W,{ref:p,spectra:t,xScale:M,yScale:F,width:w,height:x,highlightedId:me??void 0})}),T("svg",{width:C,height:v-pe,style:{position:"absolute",top:0,left:0},children:K("g",{transform:`translate(${k.left}, ${k.top})`,children:[T(Y,{xScale:M,yScale:F,width:w,height:x,xLabel:E.xLabel,yLabel:E.yLabel,showGrid:b.showGrid,colors:R}),T("defs",{children:T("clipPath",{id:S,children:T("rect",{x:0,y:0,width:w,height:x})})}),K("g",{clipPath:`url(#${S})`,children:[o.length>0&&T(Re,{regions:o,xScale:M,height:x,colors:R}),r.length>0&&T(we,{peaks:r,xScale:M,yScale:F,colors:R,onPeakClick:i})]}),n.length>0&&T(Me,{annotations:n,xScale:M,yScale:F,colors:R}),b.showCrosshair&&T(Ee,{position:Ft,width:w,height:x,colors:R,snapPoint:$t}),J&&T("rect",{x:M(J.xStart),y:0,width:Math.abs(M(J.xEnd)-M(J.xStart)),height:x,fill:R.regionFill,stroke:R.regionStroke,strokeWidth:1,pointerEvents:"none"}),T("rect",{ref:Ge,x:0,y:0,width:w,height:x,fill:"transparent",style:{cursor:b.showCrosshair?"crosshair":"grab"},onMouseDown:Dt,onMouseMove:$=>{rt($),It($)},onMouseUp:Ut,onMouseLeave:ot})]})})]})}),b.showLegend&&b.legendPosition==="bottom"&&T(oe,{spectra:t,theme:y,position:"bottom",onToggleVisibility:c,onHighlight:et,highlightedId:me})]})}import{useMemo as Ur}from"react";function _e(e,t,r={}){let{prominence:o=.01,minDistance:n=5,maxPeaks:i}=r;if(e.length<3||t.length<3)return[];let l=1/0,s=-1/0;for(let a=0;a<t.length;a++)t[a]<l&&(l=t[a]),t[a]>s&&(s=t[a]);let c=s-l;if(c===0)return[];let m=o*c,u=[];for(let a=1;a<t.length-1;a++)if(t[a]>t[a-1]&&t[a]>t[a+1]){let d=Ar(t,a),g=Dr(t,a),S=t[a]-Math.max(d,g);S>=m&&u.push({index:a,prom:S})}u.sort((a,d)=>d.prom-a.prom);let p=[];for(let a of u)p.some(g=>Math.abs(g.index-a.index)<n)||p.push(a);return(i?p.slice(0,i):p).map(a=>({x:e[a.index],y:t[a.index],label:Ir(e[a.index])})).sort((a,d)=>a.x-d.x)}function Ar(e,t){let r=e[t];for(let o=t-1;o>=0&&!(e[o]>e[t]);o--)e[o]<r&&(r=e[o]);return r}function Dr(e,t){let r=e[t];for(let o=t+1;o<e.length&&!(e[o]>e[t]);o++)e[o]<r&&(r=e[o]);return r}function Ir(e){return Math.round(e).toString()}function Fr(e,t={}){let{enabled:r=!0,spectrumIds:o,prominence:n,minDistance:i,maxPeaks:l}=t;return Ur(()=>{if(!r)return[];let s=o?e.filter(m=>o.includes(m.id)):e,c=[];for(let m of s){if(m.visible===!1)continue;let u=_e(m.x,m.y,{prominence:n,minDistance:i,maxPeaks:l});for(let p of u)c.push({...p,spectrumId:m.id})}return c},[e,r,o,n,i,l])}import{useCallback as _,useState as Ye}from"react";var wt=0,$r=[" ",",",";"," "];function kt(e){let t=e.trim().split(/\r?\n/).slice(0,5),r=",",o=0;for(let n of $r){let i=t.map(s=>s.split(n).length-1),l=Math.min(...i);l>0&&l>=o&&(i.every(c=>c===i[0])||l>o)&&(o=l,r=n)}return r}function Be(e,t={}){let{xColumn:r=0,yColumn:o=1,hasHeader:n=!0,label:i="CSV Spectrum"}=t,l=t.delimiter??kt(e),s=e.trim().split(/\r?\n/);if(s.length<2)throw new Error("CSV file must contain at least 2 lines");let c=i,m=0;if(n){let f=s[0].split(l).map(a=>a.trim());!t.label&&f[o]&&(c=f[o]),m=1}let u=[],p=[];for(let f=m;f<s.length;f++){let a=s[f].trim();if(a===""||a.startsWith("#"))continue;let d=a.split(l),g=parseFloat(d[r]),S=parseFloat(d[o]);!isNaN(g)&&!isNaN(S)&&(u.push(g),p.push(S))}if(u.length===0)throw new Error("No valid numeric data found in CSV");return{id:`csv-${++wt}`,label:c,x:new Float64Array(u),y:new Float64Array(p)}}function Or(e,t={}){let{hasHeader:r=!0,label:o}=t,n=t.delimiter??kt(e),i=e.trim().split(/\r?\n/);if(i.length<2)throw new Error("CSV file must contain at least 2 lines");let s=i[r?1:0].split(n).length;if(s<2)throw new Error("CSV must have at least 2 columns (x + y)");let c,m=0;r&&(c=i[0].split(n).map(a=>a.trim()),m=1);let u=[],p=Array.from({length:s-1},()=>[]);for(let a=m;a<i.length;a++){let d=i[a].trim();if(d===""||d.startsWith("#"))continue;let g=d.split(n),S=parseFloat(g[0]);if(!isNaN(S)){u.push(S);for(let b=1;b<s;b++){let C=parseFloat(g[b]);p[b-1].push(isNaN(C)?0:C)}}}let f=new Float64Array(u);return p.map((a,d)=>({id:`csv-${++wt}`,label:o??c?.[d+1]??`Spectrum ${d+1}`,x:f,y:new Float64Array(a)}))}var Nr=0;function He(e){let t;try{t=JSON.parse(e)}catch{throw new Error("Invalid JSON: failed to parse input")}if(Array.isArray(t))return t.map((r,o)=>Xe(r,o));if(typeof t=="object"&&t!==null){let r=t;return Array.isArray(r.spectra)?r.spectra.map((o,n)=>Xe(o,n)):[Xe(t,0)]}throw new Error("Invalid JSON structure: expected an object or array")}function Xe(e,t){let r=e.x??e.wavenumbers??e.wavelengths;if(!r||!Array.isArray(r))throw new Error(`Spectrum ${t}: missing x-axis data (expected "x", "wavenumbers", or "wavelengths")`);let o=e.y??e.intensities??e.absorbance;if(!o||!Array.isArray(o))throw new Error(`Spectrum ${t}: missing y-axis data (expected "y", "intensities", or "absorbance")`);if(r.length!==o.length)throw new Error(`Spectrum ${t}: x and y arrays must have the same length (got ${r.length} and ${o.length})`);let n=e.label??e.title??e.name??`Spectrum ${t+1}`;return{id:`json-${++Nr}`,label:n,x:new Float64Array(r),y:new Float64Array(o),xUnit:e.xUnit,yUnit:e.yUnit,type:e.type,meta:e.meta}}var Tt=0,ie=null,Rt=!1;async function zr(){if(Rt)return ie;Rt=!0;try{ie=await import("jcampconverter")}catch{ie=null}return ie}function Et(e){let t=(e["DATA TYPE"]??e.DATATYPE??"").toLowerCase();return t.includes("infrared")||t.includes("ir")?"IR":t.includes("raman")?"Raman":t.includes("nir")||t.includes("near")?"NIR":t.includes("uv")||t.includes("vis")?"UV-Vis":t.includes("fluor")?"fluorescence":"other"}async function We(e){let t=await zr();return t?Vr(e,t):[Zr(e)]}function Vr(e,t){return t.convert(e,{keepRecordsRegExp:/.*/}).flatten.map((o,n)=>{let i=o.spectra?.[0]?.data?.[0];if(!i)throw new Error(`JCAMP block ${n}: no spectral data found`);return{id:`jcamp-${++Tt}`,label:o.info?.TITLE??`Spectrum ${n+1}`,x:new Float64Array(i.x),y:new Float64Array(i.y),xUnit:o.info?.XUNITS??"cm\u207B\xB9",yUnit:o.info?.YUNITS??"Absorbance",type:Et(o.info),meta:o.info}})}function Zr(e){let t=e.split(/\r?\n/),r={},o=[],n=[],i=!1;for(let l of t){let s=l.trim();if(s.startsWith("##")){let c=s.match(/^##(.+?)=\s*(.*)$/);if(c){let m=c[1].trim().toUpperCase(),u=c[2].trim();if(m==="XYDATA"||m==="XYPOINTS"){i=!0;continue}if(m==="END"){i=!1;continue}r[m]=u}continue}if(i&&s!==""){let c=s.split(/[\s,]+/).map(Number);if(c.length>=2&&!c.some(isNaN)){let m=c[0],u=parseFloat(r.FIRSTX??"0"),p=parseFloat(r.LASTX??"0"),f=parseInt(r.NPOINTS??"0",10);if(f>0&&c.length===2)o.push(c[0]),n.push(c[1]);else if(c.length>1){let a=f>1?(p-u)/(f-1):0;for(let d=1;d<c.length;d++)o.push(m+(d-1)*a),n.push(c[d])}}}}if(o.length===0)throw new Error("Failed to parse JCAMP-DX: no data found. Install jcampconverter for full format support.");return{id:`jcamp-${++Tt}`,label:r.TITLE??"JCAMP Spectrum",x:new Float64Array(o),y:new Float64Array(n),xUnit:r.XUNITS??"cm\u207B\xB9",yUnit:r.YUNITS??"Absorbance",type:Et(r),meta:r}}function _r(e){switch(e.toLowerCase().split(".").pop()){case"dx":case"jdx":case"jcamp":return"jcamp";case"csv":case"tsv":case"txt":return"csv";case"json":return"json";default:return null}}function Br(e=[]){let[t,r]=Ye(e),[o,n]=Ye(!1),[i,l]=Ye(null),s=_(async(a,d)=>{n(!0),l(null);try{let g;switch(d){case"jcamp":g=await We(a);break;case"csv":g=[Be(a)];break;case"json":g=He(a);break}r(S=>[...S,...g])}catch(g){let S=g instanceof Error?g.message:"Failed to parse file";l(S)}finally{n(!1)}},[]),c=_(async a=>{let d=_r(a.name);if(!d){l(`Unsupported file format: ${a.name}`);return}let g=await a.text();await s(g,d)},[s]),m=_(a=>{r(d=>[...d,a])},[]),u=_(a=>{r(d=>d.filter(g=>g.id!==a))},[]),p=_(a=>{r(d=>d.map(g=>g.id===a?{...g,visible:g.visible===!1}:g))},[]),f=_(()=>{r([]),l(null)},[]);return{spectra:t,loading:o,error:i,loadFile:c,loadText:s,addSpectrum:m,removeSpectrum:u,toggleVisibility:p,clear:f}}import{useCallback as se}from"react";var Mt={solid:"",dashed:"8 4",dotted:"2 2","dash-dot":"8 4 2 4"};function je(e,t,r,o){let{width:n,height:i,background:l="#ffffff",title:s}=o,c=e.filter(m=>m.visible!==!1).map((m,u)=>{let p=m.color??A(u),f=m.lineStyle??"solid",a=m.lineWidth??1.5,d=Mt[f]??"",g=Math.min(m.x.length,m.y.length);if(g<2)return"";let S=[];for(let b=0;b<g;b++){let C=t(m.x[b]).toFixed(2),v=r(m.y[b]).toFixed(2);S.push(`${b===0?"M":"L"}${C},${v}`)}return`<path d="${S.join(" ")}" fill="none" stroke="${p}" stroke-width="${a}"${d?` stroke-dasharray="${d}"`:""}/>
3
+ <!-- ${m.label} -->`}).filter(Boolean).join(`
4
+ `);return`<?xml version="1.0" encoding="UTF-8"?>
5
+ <svg xmlns="http://www.w3.org/2000/svg" width="${n}" height="${i}" viewBox="0 0 ${n} ${i}">
6
+ <rect width="${n}" height="${i}" fill="${l}"/>
7
+ ${s?`<text x="${n/2}" y="20" text-anchor="middle" font-family="system-ui" font-size="14">${s}</text>`:""}
8
+ <g>
9
+ ${c}
10
+ </g>
11
+ </svg>`}function Ke(e,t="spectrum.svg"){let r=new Blob([e],{type:"image/svg+xml"}),o=URL.createObjectURL(r),n=document.createElement("a");n.href=o,n.download=t,document.body.appendChild(n),n.click(),document.body.removeChild(n),URL.revokeObjectURL(o)}function ae(e,t){let r=URL.createObjectURL(e),o=document.createElement("a");o.href=r,o.download=t,document.body.appendChild(o),o.click(),document.body.removeChild(o),URL.revokeObjectURL(r)}function Xr(){let e=se((n,i="spectrum.png")=>{n.toBlob(l=>{l&&ae(l,i)},"image/png")},[]),t=se((n,i="spectra.csv")=>{let l=n.filter(s=>s.visible!==!1);if(l.length!==0)if(l.length===1){let s=l[0],c=`${s.xUnit??"x"},${s.yUnit??"y"}
12
+ `,m=Array.from(s.x).map((p,f)=>`${p},${s.y[f]}`),u=c+m.join(`
13
+ `);ae(new Blob([u],{type:"text/csv"}),i)}else{let s=Math.max(...l.map(p=>p.x.length)),c=l.map(p=>`${p.label}_x,${p.label}_y`).join(","),m=[];for(let p=0;p<s;p++){let f=l.map(a=>p<a.x.length?`${a.x[p]},${a.y[p]}`:",");m.push(f.join(","))}let u=c+`
14
+ `+m.join(`
15
+ `);ae(new Blob([u],{type:"text/csv"}),i)}},[]),r=se((n,i="spectra.json")=>{let s=n.filter(m=>m.visible!==!1).map(m=>({label:m.label,x:Array.from(m.x),y:Array.from(m.y),xUnit:m.xUnit,yUnit:m.yUnit,type:m.type})),c=JSON.stringify(s,null,2);ae(new Blob([c],{type:"application/json"}),i)},[]),o=se((n,i,l,s,c,m="spectrum.svg")=>{let u=je(n,i,l,{width:s,height:c});Ke(u,m)},[]);return{exportPng:e,exportSvg:o,exportCsv:t,exportJson:r}}import{useCallback as Hr,useState as Wr}from"react";import{jsx as G,jsxs as Lt}from"react/jsx-runtime";var Yr=e=>({display:"inline-flex",alignItems:"center",justifyContent:"center",height:28,padding:"0 8px",border:`1px solid ${e==="dark"?"#4b5563":"#d1d5db"}`,borderRadius:4,background:e==="dark"?"#1f2937":"#ffffff",color:e==="dark"?"#d1d5db":"#374151",fontSize:12,cursor:"pointer",lineHeight:1,position:"relative"}),jr=e=>({position:"absolute",top:30,left:0,background:e==="dark"?"#1f2937":"#ffffff",border:`1px solid ${e==="dark"?"#4b5563":"#d1d5db"}`,borderRadius:4,boxShadow:"0 2px 8px rgba(0,0,0,0.15)",zIndex:200,minWidth:100,overflow:"hidden"}),le=e=>({display:"block",width:"100%",padding:"6px 12px",border:"none",background:"transparent",color:e==="dark"?"#d1d5db":"#374151",fontSize:12,textAlign:"left",cursor:"pointer"});function Kr({theme:e,onExportPng:t,onExportSvg:r,onExportCsv:o,onExportJson:n}){let[i,l]=Wr(!1),s=Hr(c=>{l(!1),c?.()},[]);return Lt("div",{style:{position:"relative",display:"inline-block"},children:[G("button",{type:"button",style:Yr(e),onClick:()=>l(!i),"aria-label":"Export","aria-expanded":i,"aria-haspopup":"true",children:"Export"}),i&&Lt("div",{style:jr(e),role:"menu",children:[t&&G("button",{type:"button",role:"menuitem",style:le(e),onClick:()=>s(t),children:"PNG Image"}),r&&G("button",{type:"button",role:"menuitem",style:le(e),onClick:()=>s(r),children:"SVG Vector"}),o&&G("button",{type:"button",role:"menuitem",style:le(e),onClick:()=>s(o),children:"CSV Data"}),n&&G("button",{type:"button",role:"menuitem",style:le(e),onClick:()=>s(n),children:"JSON Data"})]})]})}var Gr=0,Jr=1,qr=4,Qr=128,eo={0:"Arbitrary",1:"cm\u207B\xB9",2:"\xB5m",3:"nm",4:"s",5:"min",6:"Hz",7:"kHz",8:"MHz",9:"m/z",10:"Da",11:"ppm",12:"days",13:"years",14:"Raman shift (cm\u207B\xB9)",15:"eV",16:"Text label",255:"Double interferogram"},to={0:"Arbitrary",1:"Interferogram",2:"Absorbance",3:"Kubelka-Munk",4:"Counts",5:"V",6:"\xB0",7:"mA",8:"mm",9:"mV",10:"log(1/R)",11:"%",12:"Intensity",13:"Relative intensity",14:"Energy",16:"dB",19:"\xB0C",20:"\xB0F",21:"K",22:"Index of refraction [n]",23:"Extinction coeff. [k]",24:"Real",25:"Imaginary",26:"Complex",128:"Transmittance",129:"Reflectance",130:"Arbitrary (Valley to peak)",131:"Emission"};function ro(e,t){return e===1?"IR":e===14?"Raman":e===3&&(t===2||t===128)?"UV-Vis":e===2?"NIR":t===131?"fluorescence":"other"}function oo(e){let t=new DataView(e);if(e.byteLength<512)throw new Error("Invalid SPC file: too small for SPC header");let o=t.getUint8(0),n=t.getUint8(1);if(n!==75&&n!==77)throw new Error(`Unsupported SPC version: 0x${n.toString(16)}. Expected 0x4B or 0x4D.`);let i=t.getUint8(2),l=t.getUint8(3),s=t.getUint32(4,!0),c=t.getFloat64(8,!0),m=t.getFloat64(16,!0),u=t.getUint32(24,!0),p=eo[i]??"Arbitrary",f=to[l]??"Arbitrary",a=new Uint8Array(e,30,130),d=no(a),g=(o&qr)!==0,S=(o&Qr)!==0,b=(o&Jr)!==0,C=ro(i,l),v=null;if(!S&&s>0){v=new Float64Array(s);let x=s>1?(m-c)/(s-1):0;for(let R=0;R<s;R++)v[R]=c+R*x}let k=[],h=512,y=null;if(S&&!g){y=new Float64Array(s);for(let x=0;x<s;x++)y[x]=t.getFloat32(h,!0),h+=4}let w=g?u:1;for(let x=0;x<w;x++){let R,E,L=s;if(g){if(h+32>e.byteLength)break;let P=t.getFloat32(h+4,!0),ce=t.getFloat32(h+8,!0);if(L=t.getUint32(h+12,!0)||s,h+=32,S){R=new Float64Array(L);for(let O=0;O<L&&!(h+4>e.byteLength);O++)R[O]=t.getFloat32(h,!0),h+=4}else if(v)R=v;else{R=new Float64Array(L);let O=L>1?(ce-P)/(L-1):0;for(let N=0;N<L;N++)R[N]=P+N*O}}else R=y??v??new Float64Array(0);if(E=new Float64Array(L),b)for(let P=0;P<L&&!(h+2>e.byteLength);P++)E[P]=t.getInt16(h,!0),h+=2;else for(let P=0;P<L&&!(h+4>e.byteLength);P++)E[P]=t.getFloat32(h,!0),h+=4;k.push({id:`spc-${++Gr}`,label:d||`SPC Spectrum ${x+1}`,x:R,y:E,xUnit:p,yUnit:f,type:C,meta:{format:"SPC",version:n===75?"new":"old",xType:i.toString(),yType:l.toString()}})}if(k.length===0)throw new Error("Invalid SPC file: no spectra found");return k}function no(e){let t=e.indexOf(0),r=t>=0?e.slice(0,t):e;return new TextDecoder("ascii").decode(r).trim()}export{Me as AnnotationLayer,Y as AxisLayer,Ee as Crosshair,ct as DARK_THEME,Ue as DropZone,Kr as ExportMenu,Cr as KEYBOARD_SHORTCUTS,lt as LIGHT_THEME,Mt as LINE_DASH_PATTERNS,oe as Legend,we as PeakMarkers,Re as RegionSelector,he as SPECTRUM_COLORS,Lr as SpectraView,W as SpectrumCanvas,Fe as StackedView,De as Toolbar,yt as binarySearchClosest,be as computeXExtent,B as computeYExtent,ge as createXScale,X as createYScale,_e as detectPeaks,Ke as downloadSvg,Ve as generateChartDescription,je as generateSvg,A as getSpectrumColor,H as getThemeColors,ve as lttbDownsample,Be as parseCsv,Or as parseCsvMulti,We as parseJcamp,He as parseJson,oo as parseSpc,vr as prefersReducedMotion,Le as snapToNearestSpectrum,Xr as useExport,ze as useKeyboardNavigation,Fr as usePeakPicking,Oe as useRegionSelect,Ne as useResizeObserver,Br as useSpectrumData,Se as useZoomPan};
7
16
  //# sourceMappingURL=index.js.map