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/README.md +2 -1
- package/dist/index.cjs +14 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +366 -5
- package/dist/index.d.ts +366 -5
- package/dist/index.js +14 -5
- package/dist/index.js.map +1 -1
- package/package.json +19 -8
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
|
|
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
|
|
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
|
-
|
|
4
|
-
`)
|
|
5
|
-
|
|
6
|
-
|
|
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
|