spectraview 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/).
7
+
8
+ ## [0.1.0] — 2026-02-25
9
+
10
+ ### Added
11
+
12
+ - Core `<SpectraView />` component with Canvas+SVG hybrid rendering
13
+ - Zoom/pan via d3-zoom (mouse wheel, drag, double-click reset)
14
+ - Reversed x-axis support (IR wavenumber convention)
15
+ - JCAMP-DX parser (built-in basic + optional jcampconverter)
16
+ - CSV/TSV parser with auto-delimiter detection
17
+ - JSON parser with flexible key names
18
+ - Peak detection with prominence filtering
19
+ - Peak marker annotations
20
+ - Region highlighting
21
+ - Crosshair with coordinate readout
22
+ - Toolbar with zoom controls
23
+ - Light and dark themes
24
+ - Multi-spectrum overlay rendering
25
+ - High-DPI Canvas rendering
26
+ - Export hooks (PNG, CSV, JSON)
27
+ - `useZoomPan` hook for zoom/pan state management
28
+ - `usePeakPicking` hook for automatic peak detection
29
+ - `useSpectrumData` hook for file loading and state management
30
+ - `useExport` hook for data and image export
31
+ - Full TypeScript type definitions
32
+ - 52 tests (parsers, utilities, scales, colors, peaks)
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tubhyam Karthikeyan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # SpectraView
2
+
3
+ Interactive React component for vibrational spectroscopy (IR, Raman, NIR).
4
+
5
+ [![npm version](https://img.shields.io/npm/v/spectraview)](https://www.npmjs.com/package/spectraview)
6
+ [![npm downloads](https://img.shields.io/npm/dm/spectraview)](https://www.npmjs.com/package/spectraview)
7
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/spectraview)](https://bundlephobia.com/package/spectraview)
8
+ [![license](https://img.shields.io/npm/l/spectraview)](https://github.com/ktubhyam/spectraview/blob/main/LICENSE)
9
+ [![CI](https://img.shields.io/github/actions/workflow/status/ktubhyam/spectraview/ci.yml)](https://github.com/ktubhyam/spectraview/actions)
10
+
11
+ ## Features
12
+
13
+ - **High-performance rendering** — Canvas 2D for spectral data (10K+ points at 60fps), SVG for axes and annotations
14
+ - **Zoom and pan** — Mouse wheel zoom, click-drag pan, double-click reset
15
+ - **Reversed x-axis** — Standard IR wavenumber convention (high → low)
16
+ - **Peak detection** — Automatic peak picking with prominence filtering
17
+ - **Region selection** — Click-drag to highlight wavenumber regions
18
+ - **Multi-format loading** — JCAMP-DX, CSV/TSV, JSON
19
+ - **Multi-spectrum overlay** — Compare spectra with automatic color assignment
20
+ - **Crosshair** — Hover readout with coordinate display
21
+ - **Export** — PNG image, CSV data, JSON
22
+ - **Themes** — Light and dark mode
23
+ - **TypeScript** — Full type definitions included
24
+ - **Tiny bundle** — ~50-70KB min+gzip (no Plotly dependency)
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ npm install spectraview
30
+ ```
31
+
32
+ ## Quick Start
33
+
34
+ ```tsx
35
+ import { SpectraView } from "spectraview";
36
+
37
+ const spectrum = {
38
+ id: "1",
39
+ label: "Sample IR",
40
+ x: new Float64Array([4000, 3500, 3000, 2500, 2000, 1500, 1000, 500]),
41
+ y: new Float64Array([0.1, 0.3, 0.8, 0.2, 0.5, 0.9, 0.4, 0.1]),
42
+ xUnit: "cm⁻¹",
43
+ yUnit: "Absorbance",
44
+ };
45
+
46
+ function App() {
47
+ return <SpectraView spectra={[spectrum]} reverseX />;
48
+ }
49
+ ```
50
+
51
+ ## Loading Files
52
+
53
+ ```tsx
54
+ import { useSpectrumData, SpectraView } from "spectraview";
55
+
56
+ function App() {
57
+ const { spectra, loadFile } = useSpectrumData();
58
+
59
+ const handleDrop = (e: React.DragEvent) => {
60
+ e.preventDefault();
61
+ const file = e.dataTransfer.files[0];
62
+ if (file) loadFile(file);
63
+ };
64
+
65
+ return (
66
+ <div onDrop={handleDrop} onDragOver={(e) => e.preventDefault()}>
67
+ <SpectraView spectra={spectra} reverseX showCrosshair />
68
+ </div>
69
+ );
70
+ }
71
+ ```
72
+
73
+ ## Peak Detection
74
+
75
+ ```tsx
76
+ import { SpectraView, usePeakPicking } from "spectraview";
77
+
78
+ function App() {
79
+ const spectra = [/* your spectra */];
80
+ const peaks = usePeakPicking(spectra, {
81
+ prominence: 0.05,
82
+ minDistance: 10,
83
+ maxPeaks: 20,
84
+ });
85
+
86
+ return (
87
+ <SpectraView
88
+ spectra={spectra}
89
+ peaks={peaks}
90
+ onPeakClick={(peak) => console.log("Clicked:", peak.x)}
91
+ reverseX
92
+ />
93
+ );
94
+ }
95
+ ```
96
+
97
+ ## API Reference
98
+
99
+ ### `<SpectraView />`
100
+
101
+ | Prop | Type | Default | Description |
102
+ |------|------|---------|-------------|
103
+ | `spectra` | `Spectrum[]` | required | Array of spectra to display |
104
+ | `width` | `number` | `800` | Width in pixels |
105
+ | `height` | `number` | `400` | Height in pixels |
106
+ | `reverseX` | `boolean` | `false` | Reverse x-axis (IR convention) |
107
+ | `showGrid` | `boolean` | `true` | Show grid lines |
108
+ | `showCrosshair` | `boolean` | `true` | Show hover crosshair |
109
+ | `showToolbar` | `boolean` | `true` | Show zoom controls |
110
+ | `peaks` | `Peak[]` | `[]` | Peak markers to display |
111
+ | `regions` | `Region[]` | `[]` | Highlighted regions |
112
+ | `xLabel` | `string` | auto | X-axis label |
113
+ | `yLabel` | `string` | auto | Y-axis label |
114
+ | `theme` | `"light" \| "dark"` | `"light"` | Color theme |
115
+ | `onPeakClick` | `(peak: Peak) => void` | — | Peak click callback |
116
+ | `onViewChange` | `(view: ViewState) => void` | — | Zoom/pan callback |
117
+ | `onCrosshairMove` | `(x, y) => void` | — | Crosshair move callback |
118
+
119
+ ### Parsers
120
+
121
+ ```ts
122
+ import { parseJcamp, parseCsv, parseJson } from "spectraview";
123
+
124
+ const spectra = await parseJcamp(jcampText); // JCAMP-DX (.dx, .jdx)
125
+ const spectrum = parseCsv(csvText); // CSV/TSV
126
+ const spectra = parseJson(jsonText); // JSON
127
+ ```
128
+
129
+ ### Hooks
130
+
131
+ - **`useSpectrumData()`** — File loading and spectrum state management
132
+ - **`useZoomPan(options)`** — Zoom/pan behavior backed by d3-zoom
133
+ - **`usePeakPicking(spectra, options)`** — Automatic peak detection
134
+ - **`useExport()`** — PNG, CSV, JSON export functions
135
+
136
+ ## Companion: SpectraKit (Python)
137
+
138
+ SpectraView pairs with [SpectraKit](https://github.com/ktubhyam/spectrakit), a Python library for spectral data processing:
139
+
140
+ - **SpectraKit** — Process spectra: baseline correction, normalization, despiking, similarity
141
+ - **SpectraView** — View spectra: interactive visualization in the browser
142
+
143
+ ```
144
+ pip install spectrakit # Process in Python
145
+ npm install spectraview # View in the browser
146
+ ```
147
+
148
+ ## Browser Support
149
+
150
+ Chrome, Firefox, Safari, Edge (last 2 versions).
151
+
152
+ ## Contributing
153
+
154
+ See [CONTRIBUTING.md](./CONTRIBUTING.md).
155
+
156
+ ## License
157
+
158
+ [MIT](./LICENSE) — Tubhyam Karthikeyan
package/dist/index.cjs ADDED
@@ -0,0 +1,7 @@
1
+ "use client";
2
+ "use strict";var le=Object.defineProperty;var Be=Object.getOwnPropertyDescriptor;var He=Object.getOwnPropertyNames;var Je=Object.prototype.hasOwnProperty;var Ye=(e,t)=>{for(var o in t)le(e,o,{get:t[o],enumerable:!0})},We=(e,t,o,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of He(t))!Je.call(e,n)&&n!==o&&le(e,n,{get:()=>t[n],enumerable:!(r=Be(t,n))||r.enumerable});return e};var Ge=e=>We(le({},"__esModule",{value:!0}),e);var gt={};Ye(gt,{AxisLayer:()=>Y,Crosshair:()=>K,DARK_THEME:()=>pe,LIGHT_THEME:()=>ue,PeakMarkers:()=>W,RegionSelector:()=>G,SPECTRUM_COLORS:()=>X,SpectraView:()=>Re,SpectrumCanvas:()=>J,Toolbar:()=>q,computeXExtent:()=>Z,computeYExtent:()=>V,createXScale:()=>_,createYScale:()=>z,detectPeaks:()=>Q,getSpectrumColor:()=>j,getThemeColors:()=>B,parseCsv:()=>ee,parseCsvMulti:()=>Ie,parseJcamp:()=>oe,parseJson:()=>te,useExport:()=>$e,usePeakPicking:()=>Ae,useSpectrumData:()=>Ue,useZoomPan:()=>H});module.exports=Ge(gt);var x=require("react");var ce=require("d3-scale"),me=require("d3-array"),Ke=.05;function Z(e){let t=1/0,o=-1/0;for(let r of e){if(r.visible===!1)continue;let[n,i]=(0,me.extent)(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]=(0,me.extent)(i.y);m<t&&(t=m),c>o&&(o=c)}if(!isFinite(t))return[0,1];let n=(o-t)*Ke;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(0,ce.scaleLinear)().domain(i).range([0,n])}function z(e,t,o){let r=t-o.top-o.bottom;return(0,ce.scaleLinear)().domain(e).range([r,0])}var X=["#2563eb","#dc2626","#16a34a","#9333ea","#ea580c","#0891b2","#be185d","#854d0e","#4f46e5","#65a30d"],ue={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"},pe={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"?pe:ue}var C=require("react"),M=require("d3-zoom"),T=require("d3-selection"),St=require("d3-transition"),ve=1.5;function H(e){let{plotWidth:t,plotHeight:o,xScale:r,yScale:n,scaleExtent:i=[1,50],enabled:m=!0,onViewChange:c}=e,s=(0,C.useRef)(null),u=(0,C.useRef)(null),a=(0,C.useRef)(c);a.current=c;let b=(0,C.useRef)(i);b.current=i;let[p,l]=(0,C.useState)(M.zoomIdentity),f=(0,C.useMemo)(()=>p.rescaleX(r.copy()),[p,r]),d=(0,C.useMemo)(()=>p.rescaleY(n.copy()),[p,n]);(0,C.useEffect)(()=>{let v=s.current;if(!v||!m)return;let E=(0,M.zoom)().scaleExtent(b.current).extent([[0,0],[t,o]]).translateExtent([[-1/0,-1/0],[1/0,1/0]]).on("zoom",ae=>{let D=ae.transform;if(l(D),a.current){let N=D.rescaleX(r.copy()),ie=D.rescaleY(n.copy());a.current(N.domain(),ie.domain())}});return u.current=E,(0,T.select)(v).call(E),(0,T.select)(v).on("dblclick.zoom",()=>{(0,T.select)(v).transition().duration(300).call(E.transform,M.zoomIdentity)}),()=>{(0,T.select)(v).on(".zoom",null)}},[t,o,m,r,n]);let g=(0,C.useCallback)(()=>{!s.current||!u.current||(0,T.select)(s.current).transition().duration(300).call(u.current.transform,M.zoomIdentity)},[]),y=(0,C.useCallback)(()=>{!s.current||!u.current||(0,T.select)(s.current).transition().duration(200).call(u.current.scaleBy,ve)},[]),w=(0,C.useCallback)(()=>{!s.current||!u.current||(0,T.select)(s.current).transition().duration(200).call(u.current.scaleBy,1/ve)},[]);return{zoomRef:s,state:{transform:p,isZoomed:p.k!==1||p.x!==0||p.y!==0},zoomedXScale:f,zoomedYScale:d,resetZoom:g,zoomIn:y,zoomOut:w}}var A=require("react");var qe=1.5,Qe=2.5;function et(e,t,o){e.clearRect(0,0,t,o)}function tt(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?Qe:qe,[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 y=t.x[g];if(y<l&&g<s-1&&t.x[g+1]<l||y>f&&g>0&&t.x[g-1]>f)continue;let w=r(y),v=n(t.y[g]);d?e.lineTo(w,v):(e.moveTo(w,v),d=!0)}e.stroke(),e.restore()}function ke(e,t,o,r,n,i,m){et(e,n,i),t.forEach((c,s)=>{c.visible!==!1&&tt(e,c,s,o,r,{highlighted:c.id===m,opacity:m&&c.id!==m?.3:1})})}var we=require("react/jsx-runtime");function J({spectra:e,xScale:t,yScale:o,width:r,height:n,highlightedId:i}){let m=(0,A.useRef)(null),c=(0,A.useRef)(1);return(0,A.useEffect)(()=>{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]),(0,A.useEffect)(()=>{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),ke(u,e,t,o,r,n,i)},[e,t,o,r,n,i]),(0,we.jsx)("canvas",{ref:m,style:{width:r,height:n,position:"absolute",top:0,left:0,pointerEvents:"none"}})}var S=require("react/jsx-runtime");function Te(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 Pe(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:o,height:r,xLabel:n,yLabel:i,showGrid:m=!0,colors:c}){let s=Te(e,8),u=Te(t,6);return(0,S.jsxs)("g",{children:[m&&(0,S.jsxs)("g",{children:[s.map(a=>(0,S.jsx)("line",{x1:e(a),x2:e(a),y1:0,y2:r,stroke:c.gridColor,strokeWidth:.5},`xgrid-${a}`)),u.map(a=>(0,S.jsx)("line",{x1:0,x2:o,y1:t(a),y2:t(a),stroke:c.gridColor,strokeWidth:.5},`ygrid-${a}`))]}),(0,S.jsxs)("g",{transform:`translate(0, ${r})`,children:[(0,S.jsx)("line",{x1:0,x2:o,y1:0,y2:0,stroke:c.axisColor}),s.map(a=>(0,S.jsxs)("g",{transform:`translate(${e(a)}, 0)`,children:[(0,S.jsx)("line",{y1:0,y2:6,stroke:c.axisColor}),(0,S.jsx)("text",{y:20,textAnchor:"middle",fill:c.tickColor,fontSize:11,fontFamily:"system-ui, sans-serif",children:Pe(a)})]},`xtick-${a}`)),n&&(0,S.jsx)("text",{x:o/2,y:42,textAnchor:"middle",fill:c.labelColor,fontSize:13,fontFamily:"system-ui, sans-serif",children:n})]}),(0,S.jsxs)("g",{children:[(0,S.jsx)("line",{x1:0,x2:0,y1:0,y2:r,stroke:c.axisColor}),u.map(a=>(0,S.jsxs)("g",{transform:`translate(0, ${t(a)})`,children:[(0,S.jsx)("line",{x1:-6,x2:0,stroke:c.axisColor}),(0,S.jsx)("text",{x:-10,textAnchor:"end",dominantBaseline:"middle",fill:c.tickColor,fontSize:11,fontFamily:"system-ui, sans-serif",children:Pe(a)})]},`ytick-${a}`)),i&&(0,S.jsx)("text",{transform:`translate(-50, ${r/2}) rotate(-90)`,textAnchor:"middle",fill:c.labelColor,fontSize:13,fontFamily:"system-ui, sans-serif",children:i})]})]})}var L=require("react/jsx-runtime");function W({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(0,L.jsx)("g",{className:"spectraview-peaks",children:u.map((a,b)=>{let p=t(a.x),l=o(a.y);return(0,L.jsxs)("g",{transform:`translate(${p}, ${l})`,style:{cursor:n?"pointer":"default"},onClick:()=>n?.(a),children:[(0,L.jsx)("polygon",{points:`0,-5 -5,${-5*2.5} 5,${-5*2.5}`,fill:r.labelColor,opacity:.8}),a.label&&(0,L.jsx)("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}`)})})}var I=require("react/jsx-runtime");function G({regions:e,xScale:t,height:o,colors:r}){return(0,I.jsx)("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(0,I.jsxs)("g",{children:[(0,I.jsx)("rect",{x:s,y:0,width:u,height:o,fill:n.color??r.regionFill,stroke:r.regionStroke,strokeWidth:1}),n.label&&(0,I.jsx)("text",{x:s+u/2,y:12,textAnchor:"middle",fill:r.labelColor,fontSize:10,fontFamily:"system-ui, sans-serif",children:n.label})]},`region-${i}`)})})}var P=require("react/jsx-runtime");function K({position:e,width:t,height:o,colors:r}){return e?(0,P.jsxs)("g",{className:"spectraview-crosshair",pointerEvents:"none",children:[(0,P.jsx)("line",{x1:e.px,x2:e.px,y1:0,y2:o,stroke:r.crosshairColor,strokeWidth:1,strokeDasharray:"4 4"}),(0,P.jsx)("line",{x1:0,x2:t,y1:e.py,y2:e.py,stroke:r.crosshairColor,strokeWidth:1,strokeDasharray:"4 4"}),(0,P.jsxs)("g",{transform:`translate(${Math.min(e.px+10,t-100)}, ${Math.max(e.py-10,20)})`,children:[(0,P.jsx)("rect",{x:0,y:-14,width:90,height:18,rx:3,fill:r.tooltipBg,stroke:r.tooltipBorder,strokeWidth:.5,opacity:.9}),(0,P.jsxs)("text",{x:5,y:0,fill:r.tooltipText,fontSize:10,fontFamily:"monospace",children:[Ee(e.dataX),", ",Ee(e.dataY)]})]})]}):null}function Ee(e){return Math.abs(e)>=100?Math.round(e).toString():Math.abs(e)>=1?e.toFixed(1):e.toFixed(4)}var F=require("react/jsx-runtime"),fe=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}),rt=e=>({display:"flex",gap:4,padding:"4px 0",borderBottom:`1px solid ${e==="dark"?"#374151":"#e5e7eb"}`});function q({onZoomIn:e,onZoomOut:t,onReset:o,isZoomed:r,theme:n}){return(0,F.jsxs)("div",{style:rt(n),className:"spectraview-toolbar",children:[(0,F.jsx)("button",{type:"button",style:fe(n),onClick:e,title:"Zoom in","aria-label":"Zoom in",children:"+"}),(0,F.jsx)("button",{type:"button",style:fe(n),onClick:t,title:"Zoom out","aria-label":"Zoom out",children:"\u2212"}),(0,F.jsx)("button",{type:"button",style:{...fe(n),opacity:r?1:.4},onClick:o,disabled:!r,title:"Reset zoom","aria-label":"Reset zoom",children:"\u21BA"})]})}var h=require("react/jsx-runtime"),ot={top:20,right:20,bottom:50,left:65},nt=800,st=400;function at(e){return{width:e.width??nt,height:e.height??st,reverseX:e.reverseX??!1,showGrid:e.showGrid??!0,showCrosshair:e.showCrosshair??!0,showToolbar:e.showToolbar??!0,displayMode:e.displayMode??"overlay",margin:{...ot,...e.margin},theme:e.theme??"light"}}function it(e,t,o){let r=e[0];return{xLabel:t??r?.xUnit??"x",yLabel:o??r?.yUnit??"y"}}function Re(e){let{spectra:t,peaks:o=[],regions:r=[],onPeakClick:n,onViewChange:i,onCrosshairMove:m}=e,s=`spectraview-clip-${(0,x.useId)().replace(/:/g,"")}`,u=(0,x.useMemo)(()=>at(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,y=(0,x.useMemo)(()=>B(f),[f]),w=(0,x.useMemo)(()=>it(t,e.xLabel,e.yLabel),[t,e.xLabel,e.yLabel]),v=(0,x.useMemo)(()=>Z(t),[t]),E=(0,x.useMemo)(()=>V(t),[t]),ae=(0,x.useMemo)(()=>_(v,a,p,l),[v,a,p,l]),D=(0,x.useMemo)(()=>z(E,b,p),[E,b,p]),N=(0,x.useRef)(i);N.current=i;let ie=(0,x.useMemo)(()=>($,O)=>{N.current?.({xDomain:$,yDomain:O})},[]),{zoomRef:Ne,state:Oe,zoomedXScale:R,zoomedYScale:U,resetZoom:Ze,zoomIn:Ve,zoomOut:_e}=H({plotWidth:d,plotHeight:g,xScale:ae,yScale:D,onViewChange:i?ie:void 0}),[ze,be]=(0,x.useState)(null),ge=(0,x.useRef)(m);ge.current=m;let Xe=(0,x.useCallback)($=>{if(!u.showCrosshair)return;let O=$.currentTarget.getBoundingClientRect(),xe=$.clientX-O.left,ye=$.clientY-O.top,Se=R.invert(xe),Ce=U.invert(ye);be({px:xe,py:ye,dataX:Se,dataY:Ce}),ge.current?.(Se,Ce)},[R,U,u.showCrosshair]),je=(0,x.useCallback)(()=>{be(null)},[]);if(t.length===0)return(0,h.jsx)("div",{style:{width:a,height:b,display:"flex",alignItems:"center",justifyContent:"center",border:`1px dashed ${y.gridColor}`,borderRadius:8,color:y.tickColor,fontFamily:"system-ui, sans-serif",fontSize:14},className:e.className,children:"No spectra loaded"});let he=u.showToolbar?37:0;return(0,h.jsxs)("div",{style:{width:a,background:y.background,borderRadius:4,overflow:"hidden"},className:e.className,children:[u.showToolbar&&(0,h.jsx)(q,{onZoomIn:Ve,onZoomOut:_e,onReset:Ze,isZoomed:Oe.isZoomed,theme:f}),(0,h.jsxs)("div",{style:{position:"relative",width:a,height:b-he},children:[(0,h.jsx)("div",{style:{position:"absolute",top:p.top,left:p.left,width:d,height:g,overflow:"hidden"},children:(0,h.jsx)(J,{spectra:t,xScale:R,yScale:U,width:d,height:g})}),(0,h.jsx)("svg",{width:a,height:b-he,style:{position:"absolute",top:0,left:0},children:(0,h.jsxs)("g",{transform:`translate(${p.left}, ${p.top})`,children:[(0,h.jsx)(Y,{xScale:R,yScale:U,width:d,height:g,xLabel:w.xLabel,yLabel:w.yLabel,showGrid:u.showGrid,colors:y}),(0,h.jsx)("defs",{children:(0,h.jsx)("clipPath",{id:s,children:(0,h.jsx)("rect",{x:0,y:0,width:d,height:g})})}),(0,h.jsxs)("g",{clipPath:`url(#${s})`,children:[r.length>0&&(0,h.jsx)(G,{regions:r,xScale:R,height:g,colors:y}),o.length>0&&(0,h.jsx)(W,{peaks:o,xScale:R,yScale:U,colors:y,onPeakClick:n})]}),u.showCrosshair&&(0,h.jsx)(K,{position:ze,width:d,height:g,colors:y}),(0,h.jsx)("rect",{ref:Ne,x:0,y:0,width:d,height:g,fill:"transparent",style:{cursor:u.showCrosshair?"crosshair":"grab"},onMouseMove:Xe,onMouseLeave:je})]})})]})]})}var Me=require("react");function Q(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=lt(t,l),d=ct(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:mt(e[l.index])})).sort((l,f)=>l.x-f.x)}function lt(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 ct(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 mt(e){return Math.round(e).toString()}function Ae(e,t={}){let{enabled:o=!0,spectrumIds:r,prominence:n,minDistance:i,maxPeaks:m}=t;return(0,Me.useMemo)(()=>{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=Q(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])}var k=require("react");var ut=[" ",",",";"," "];function Le(e){let t=e.trim().split(/\r?\n/).slice(0,5),o=",",r=0;for(let n of ut){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 ee(e,t={}){let{xColumn:o=0,yColumn:r=1,hasHeader:n=!0,label:i="CSV Spectrum"}=t,m=t.delimiter??Le(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 Ie(e,t={}){let{hasHeader:o=!0,label:r}=t,n=t.delimiter??Le(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 y=1;y<c;y++){let w=parseFloat(d[y]);b[y-1].push(isNaN(w)?0:w)}}}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 te(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)=>de(o,r));if(typeof t=="object"&&t!==null){let o=t;return Array.isArray(o.spectra)?o.spectra.map((r,n)=>de(r,n)):[de(t,0)]}throw new Error("Invalid JSON structure: expected an object or array")}function de(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 re=null,Fe=!1;async function pt(){if(Fe)return re;Fe=!0;try{re=await import("jcampconverter")}catch{re=null}return re}function De(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 oe(e){let t=await pt();return t?ft(e,t):[dt(e)]}function ft(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:De(r.info),meta:r.info}})}function dt(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:De(o),meta:o}}function bt(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 Ue(e=[]){let[t,o]=(0,k.useState)(e),[r,n]=(0,k.useState)(!1),[i,m]=(0,k.useState)(null),c=(0,k.useCallback)(async(l,f)=>{n(!0),m(null);try{let d;switch(f){case"jcamp":d=await oe(l);break;case"csv":d=[ee(l)];break;case"json":d=te(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=(0,k.useCallback)(async l=>{let f=bt(l.name);if(!f){m(`Unsupported file format: ${l.name}`);return}let d=await l.text();await c(d,f)},[c]),u=(0,k.useCallback)(l=>{o(f=>[...f,l])},[]),a=(0,k.useCallback)(l=>{o(f=>f.filter(d=>d.id!==l))},[]),b=(0,k.useCallback)(l=>{o(f=>f.map(d=>d.id===l?{...d,visible:d.visible===!1}:d))},[]),p=(0,k.useCallback)(()=>{o([]),m(null)},[]);return{spectra:t,loading:r,error:i,loadFile:s,loadText:c,addSpectrum:u,removeSpectrum:a,toggleVisibility:b,clear:p}}var se=require("react");function ne(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 $e(){let e=(0,se.useCallback)((r,n="spectrum.png")=>{r.toBlob(i=>{i&&ne(i,n)},"image/png")},[]),t=(0,se.useCallback)((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
+ `);ne(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
+ `);ne(new Blob([u],{type:"text/csv"}),n)}},[]),o=(0,se.useCallback)((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);ne(new Blob([c],{type:"application/json"}),n)},[]);return{exportPng:e,exportCsv:t,exportJson:o}}0&&(module.exports={AxisLayer,Crosshair,DARK_THEME,LIGHT_THEME,PeakMarkers,RegionSelector,SPECTRUM_COLORS,SpectraView,SpectrumCanvas,Toolbar,computeXExtent,computeYExtent,createXScale,createYScale,detectPeaks,getSpectrumColor,getThemeColors,parseCsv,parseCsvMulti,parseJcamp,parseJson,useExport,usePeakPicking,useSpectrumData,useZoomPan});
7
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/components/SpectraView/SpectraView.tsx","../src/utils/scales.ts","../src/utils/colors.ts","../src/hooks/useZoomPan.ts","../src/components/SpectrumCanvas/SpectrumCanvas.tsx","../src/utils/rendering.ts","../src/components/AxisLayer/AxisLayer.tsx","../src/components/PeakMarkers/PeakMarkers.tsx","../src/components/RegionSelector/RegionSelector.tsx","../src/components/Crosshair/Crosshair.tsx","../src/components/Toolbar/Toolbar.tsx","../src/hooks/usePeakPicking.ts","../src/utils/peaks.ts","../src/hooks/useSpectrumData.ts","../src/parsers/csv.ts","../src/parsers/json.ts","../src/parsers/jcamp.ts","../src/hooks/useExport.ts"],"sourcesContent":["/**\n * SpectraView — Interactive React component for vibrational spectroscopy.\n *\n * @example\n * ```tsx\n * import { SpectraView, parseCsv } from \"spectraview\";\n *\n * const spectrum = parseCsv(csvText);\n * <SpectraView spectra={[spectrum]} reverseX />\n * ```\n *\n * @packageDocumentation\n */\n\n// Main component\nexport { SpectraView } from \"./components/SpectraView/SpectraView\";\n\n// Sub-components (for advanced composition)\nexport { SpectrumCanvas } from \"./components/SpectrumCanvas/SpectrumCanvas\";\nexport { AxisLayer } from \"./components/AxisLayer/AxisLayer\";\nexport { PeakMarkers } from \"./components/PeakMarkers/PeakMarkers\";\nexport { RegionSelector } from \"./components/RegionSelector/RegionSelector\";\nexport { Crosshair } from \"./components/Crosshair/Crosshair\";\nexport { Toolbar } from \"./components/Toolbar/Toolbar\";\n\n// Hooks\nexport { useZoomPan } from \"./hooks/useZoomPan\";\nexport { usePeakPicking } from \"./hooks/usePeakPicking\";\nexport { useSpectrumData } from \"./hooks/useSpectrumData\";\nexport { useExport } from \"./hooks/useExport\";\n\n// Parsers\nexport { parseJcamp } from \"./parsers/jcamp\";\nexport { parseCsv, parseCsvMulti } from \"./parsers/csv\";\nexport { parseJson } from \"./parsers/json\";\n\n// Utilities\nexport { detectPeaks } from \"./utils/peaks\";\nexport {\n computeXExtent,\n computeYExtent,\n createXScale,\n createYScale,\n} from \"./utils/scales\";\nexport {\n SPECTRUM_COLORS,\n LIGHT_THEME,\n DARK_THEME,\n getSpectrumColor,\n getThemeColors,\n} from \"./utils/colors\";\n\n// Types (re-export all)\nexport type {\n Spectrum,\n SpectrumType,\n Peak,\n Region,\n ViewState,\n Theme,\n DisplayMode,\n Margin,\n SpectraViewProps,\n ResolvedConfig,\n} from \"./types\";\n\nexport type { CsvParseOptions } from \"./parsers/csv\";\nexport type { PeakDetectionOptions } from \"./utils/peaks\";\nexport type {\n UseZoomPanOptions,\n UseZoomPanReturn,\n ZoomPanState,\n} from \"./hooks/useZoomPan\";\nexport type { UsePeakPickingOptions } from \"./hooks/usePeakPicking\";\nexport type { UseSpectrumDataReturn } from \"./hooks/useSpectrumData\";\nexport type { UseExportReturn } from \"./hooks/useExport\";\nexport type { CrosshairPosition, CrosshairProps } from \"./components/Crosshair/Crosshair\";\n","/**\n * SpectraView — Main interactive spectrum viewer component.\n *\n * Composes the Canvas data layer, SVG axis/annotation layer,\n * crosshair, peak markers, region selection, toolbar, and zoom/pan\n * into a single embeddable React component.\n *\n * Architecture:\n * - Canvas layer: high-performance spectral line rendering (10K+ points)\n * - SVG layer: axes, grid, annotations, crosshair (lightweight interactive elements)\n * - d3-zoom: zoom/pan math via useZoomPan hook\n */\n\nimport { useCallback, useId, useMemo, useRef, useState } from \"react\";\nimport type {\n SpectraViewProps,\n ResolvedConfig,\n Margin,\n Spectrum,\n} from \"../../types\";\nimport { computeXExtent, computeYExtent, createXScale, createYScale } from \"../../utils/scales\";\nimport { getThemeColors } from \"../../utils/colors\";\nimport { useZoomPan } from \"../../hooks/useZoomPan\";\nimport { SpectrumCanvas } from \"../SpectrumCanvas/SpectrumCanvas\";\nimport { AxisLayer } from \"../AxisLayer/AxisLayer\";\nimport { PeakMarkers } from \"../PeakMarkers/PeakMarkers\";\nimport { RegionSelector } from \"../RegionSelector/RegionSelector\";\nimport { Crosshair } from \"../Crosshair/Crosshair\";\nimport type { CrosshairPosition } from \"../Crosshair/Crosshair\";\nimport { Toolbar } from \"../Toolbar/Toolbar\";\n\n/** Default chart margins. */\nconst DEFAULT_MARGIN: Margin = {\n top: 20,\n right: 20,\n bottom: 50,\n left: 65,\n};\n\n/** Default component width. */\nconst DEFAULT_WIDTH = 800;\n\n/** Default component height. */\nconst DEFAULT_HEIGHT = 400;\n\n/**\n * Resolve user props into a complete configuration with defaults.\n */\nfunction resolveConfig(props: SpectraViewProps): ResolvedConfig {\n return {\n width: props.width ?? DEFAULT_WIDTH,\n height: props.height ?? DEFAULT_HEIGHT,\n reverseX: props.reverseX ?? false,\n showGrid: props.showGrid ?? true,\n showCrosshair: props.showCrosshair ?? true,\n showToolbar: props.showToolbar ?? true,\n displayMode: props.displayMode ?? \"overlay\",\n margin: { ...DEFAULT_MARGIN, ...props.margin },\n theme: props.theme ?? \"light\",\n };\n}\n\n/**\n * Infer axis labels from spectrum metadata if not provided.\n */\nfunction inferLabels(\n spectra: Spectrum[],\n xLabel?: string,\n yLabel?: string,\n): { xLabel: string; yLabel: string } {\n const first = spectra[0];\n return {\n xLabel: xLabel ?? first?.xUnit ?? \"x\",\n yLabel: yLabel ?? first?.yUnit ?? \"y\",\n };\n}\n\nexport function SpectraView(props: SpectraViewProps) {\n const { spectra, peaks = [], regions = [], onPeakClick, onViewChange, onCrosshairMove } =\n props;\n\n // Unique ID for this instance to avoid clipPath collisions (BUG-1 fix)\n const instanceId = useId();\n const clipId = `spectraview-clip-${instanceId.replace(/:/g, \"\")}`;\n\n const config = useMemo(() => resolveConfig(props), [\n props.width,\n props.height,\n props.reverseX,\n props.showGrid,\n props.showCrosshair,\n props.showToolbar,\n props.displayMode,\n props.margin,\n props.theme,\n ]);\n\n const { width, height, margin, reverseX, theme } = config;\n const plotWidth = width - margin.left - margin.right;\n const plotHeight = height - margin.top - margin.bottom;\n const colors = useMemo(() => getThemeColors(theme), [theme]);\n const labels = useMemo(\n () => inferLabels(spectra, props.xLabel, props.yLabel),\n [spectra, props.xLabel, props.yLabel],\n );\n\n // Compute data extents\n const xExtent = useMemo(() => computeXExtent(spectra), [spectra]);\n const yExtent = useMemo(() => computeYExtent(spectra), [spectra]);\n\n // Create base (unzoomed) scales\n const baseXScale = useMemo(\n () => createXScale(xExtent, width, margin, reverseX),\n [xExtent, width, margin, reverseX],\n );\n const baseYScale = useMemo(\n () => createYScale(yExtent, height, margin),\n [yExtent, height, margin],\n );\n\n // Stable onViewChange wrapper via ref to avoid re-attaching zoom\n const onViewChangeRef = useRef(onViewChange);\n onViewChangeRef.current = onViewChange;\n const stableOnViewChange = useMemo(\n () =>\n (xDomain: [number, number], yDomain: [number, number]) => {\n onViewChangeRef.current?.({ xDomain, yDomain });\n },\n [],\n );\n\n // Zoom/pan behavior\n const {\n zoomRef,\n state: zoomState,\n zoomedXScale,\n zoomedYScale,\n resetZoom,\n zoomIn,\n zoomOut,\n } = useZoomPan({\n plotWidth,\n plotHeight,\n xScale: baseXScale,\n yScale: baseYScale,\n onViewChange: onViewChange ? stableOnViewChange : undefined,\n });\n\n // Crosshair state — managed here so the zoom rect handles all mouse events (BUG-2 fix)\n const [crosshairPos, setCrosshairPos] = useState<CrosshairPosition | null>(null);\n const onCrosshairMoveRef = useRef(onCrosshairMove);\n onCrosshairMoveRef.current = onCrosshairMove;\n\n const handleMouseMove = useCallback(\n (event: React.MouseEvent<SVGRectElement>) => {\n if (!config.showCrosshair) return;\n const rect = event.currentTarget.getBoundingClientRect();\n const px = event.clientX - rect.left;\n const py = event.clientY - rect.top;\n const dataX = zoomedXScale.invert(px);\n const dataY = zoomedYScale.invert(py);\n setCrosshairPos({ px, py, dataX, dataY });\n onCrosshairMoveRef.current?.(dataX, dataY);\n },\n [zoomedXScale, zoomedYScale, config.showCrosshair],\n );\n\n const handleMouseLeave = useCallback(() => {\n setCrosshairPos(null);\n }, []);\n\n // Empty state\n if (spectra.length === 0) {\n return (\n <div\n style={{\n width,\n height,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n border: `1px dashed ${colors.gridColor}`,\n borderRadius: 8,\n color: colors.tickColor,\n fontFamily: \"system-ui, sans-serif\",\n fontSize: 14,\n }}\n className={props.className}\n >\n No spectra loaded\n </div>\n );\n }\n\n const toolbarHeight = config.showToolbar ? 37 : 0;\n\n return (\n <div\n style={{\n width,\n background: colors.background,\n borderRadius: 4,\n overflow: \"hidden\",\n }}\n className={props.className}\n >\n {/* Toolbar */}\n {config.showToolbar && (\n <Toolbar\n onZoomIn={zoomIn}\n onZoomOut={zoomOut}\n onReset={resetZoom}\n isZoomed={zoomState.isZoomed}\n theme={theme}\n />\n )}\n\n {/* Chart area */}\n <div\n style={{\n position: \"relative\",\n width,\n height: height - toolbarHeight,\n }}\n >\n {/* Canvas layer for spectral data (behind SVG) */}\n <div\n style={{\n position: \"absolute\",\n top: margin.top,\n left: margin.left,\n width: plotWidth,\n height: plotHeight,\n overflow: \"hidden\",\n }}\n >\n <SpectrumCanvas\n spectra={spectra}\n xScale={zoomedXScale}\n yScale={zoomedYScale}\n width={plotWidth}\n height={plotHeight}\n />\n </div>\n\n {/* SVG overlay for axes, annotations, crosshair */}\n <svg\n width={width}\n height={height - toolbarHeight}\n style={{ position: \"absolute\", top: 0, left: 0 }}\n >\n <g transform={`translate(${margin.left}, ${margin.top})`}>\n {/* Axes and grid */}\n <AxisLayer\n xScale={zoomedXScale}\n yScale={zoomedYScale}\n width={plotWidth}\n height={plotHeight}\n xLabel={labels.xLabel}\n yLabel={labels.yLabel}\n showGrid={config.showGrid}\n colors={colors}\n />\n\n {/* Clip path for plot area content */}\n <defs>\n <clipPath id={clipId}>\n <rect x={0} y={0} width={plotWidth} height={plotHeight} />\n </clipPath>\n </defs>\n\n <g clipPath={`url(#${clipId})`}>\n {/* Region highlights */}\n {regions.length > 0 && (\n <RegionSelector\n regions={regions}\n xScale={zoomedXScale}\n height={plotHeight}\n colors={colors}\n />\n )}\n\n {/* Peak markers */}\n {peaks.length > 0 && (\n <PeakMarkers\n peaks={peaks}\n xScale={zoomedXScale}\n yScale={zoomedYScale}\n colors={colors}\n onPeakClick={onPeakClick}\n />\n )}\n </g>\n\n {/* Crosshair (rendered above data, pointer-events: none) */}\n {config.showCrosshair && (\n <Crosshair\n position={crosshairPos}\n width={plotWidth}\n height={plotHeight}\n colors={colors}\n />\n )}\n\n {/* Zoom/pan + crosshair interaction rect (single surface for all mouse events) */}\n <rect\n ref={zoomRef}\n x={0}\n y={0}\n width={plotWidth}\n height={plotHeight}\n fill=\"transparent\"\n style={{ cursor: config.showCrosshair ? \"crosshair\" : \"grab\" }}\n onMouseMove={handleMouseMove}\n onMouseLeave={handleMouseLeave}\n />\n </g>\n </svg>\n </div>\n </div>\n );\n}\n","/**\n * D3 scale factories for spectral axes.\n *\n * Handles reversed x-axis (standard for IR wavenumber display)\n * and automatic domain computation from spectral data.\n */\n\nimport { scaleLinear } from \"d3-scale\";\nimport { extent } from \"d3-array\";\nimport type { Spectrum, Margin } from \"../types\";\n\n/** Padding factor applied to y-axis domain (5% on each side). */\nconst Y_PADDING = 0.05;\n\n/**\n * Compute the x-axis extent across all visible spectra.\n */\nexport function computeXExtent(spectra: Spectrum[]): [number, number] {\n let globalMin = Infinity;\n let globalMax = -Infinity;\n\n for (const s of spectra) {\n if (s.visible === false) continue;\n const [min, max] = extent(s.x as number[]) as [number, number];\n if (min < globalMin) globalMin = min;\n if (max > globalMax) globalMax = max;\n }\n\n if (!isFinite(globalMin)) return [0, 1];\n return [globalMin, globalMax];\n}\n\n/**\n * Compute the y-axis extent across all visible spectra with padding.\n */\nexport function computeYExtent(spectra: Spectrum[]): [number, number] {\n let globalMin = Infinity;\n let globalMax = -Infinity;\n\n for (const s of spectra) {\n if (s.visible === false) continue;\n const [min, max] = extent(s.y as number[]) as [number, number];\n if (min < globalMin) globalMin = min;\n if (max > globalMax) globalMax = max;\n }\n\n if (!isFinite(globalMin)) return [0, 1];\n\n const range = globalMax - globalMin;\n const pad = range * Y_PADDING;\n return [globalMin - pad, globalMax + pad];\n}\n\n/**\n * Create an x-axis scale.\n *\n * When `reverseX` is true, the domain is reversed so higher wavenumbers\n * appear on the left (standard IR convention).\n */\nexport function createXScale(\n domain: [number, number],\n width: number,\n margin: Margin,\n reverseX: boolean,\n) {\n const plotWidth = width - margin.left - margin.right;\n const d = reverseX ? [domain[1], domain[0]] : domain;\n return scaleLinear().domain(d).range([0, plotWidth]);\n}\n\n/**\n * Create a y-axis scale (always low values at bottom, high at top).\n */\nexport function createYScale(\n domain: [number, number],\n height: number,\n margin: Margin,\n) {\n const plotHeight = height - margin.top - margin.bottom;\n return scaleLinear().domain(domain).range([plotHeight, 0]);\n}\n","/**\n * Default color palette for rendering multiple spectra.\n *\n * Colors are chosen for good contrast on both light and dark backgrounds,\n * and are distinguishable for common forms of color blindness.\n */\n\n/** Default spectrum color palette (10 colors). */\nexport const SPECTRUM_COLORS = [\n \"#2563eb\", // blue\n \"#dc2626\", // red\n \"#16a34a\", // green\n \"#9333ea\", // purple\n \"#ea580c\", // orange\n \"#0891b2\", // cyan\n \"#be185d\", // pink\n \"#854d0e\", // brown\n \"#4f46e5\", // indigo\n \"#65a30d\", // lime\n] as const;\n\n/** Theme color definition. */\nexport interface ThemeColors {\n background: string;\n axisColor: string;\n gridColor: string;\n tickColor: string;\n labelColor: string;\n crosshairColor: string;\n regionFill: string;\n regionStroke: string;\n tooltipBg: string;\n tooltipBorder: string;\n tooltipText: string;\n}\n\n/** Light theme colors. */\nexport const LIGHT_THEME: ThemeColors = {\n background: \"#ffffff\",\n axisColor: \"#374151\",\n gridColor: \"#e5e7eb\",\n tickColor: \"#6b7280\",\n labelColor: \"#111827\",\n crosshairColor: \"#9ca3af\",\n regionFill: \"rgba(37, 99, 235, 0.1)\",\n regionStroke: \"rgba(37, 99, 235, 0.4)\",\n tooltipBg: \"#ffffff\",\n tooltipBorder: \"#d1d5db\",\n tooltipText: \"#111827\",\n};\n\n/** Dark theme colors. */\nexport const DARK_THEME: ThemeColors = {\n background: \"#111827\",\n axisColor: \"#d1d5db\",\n gridColor: \"#374151\",\n tickColor: \"#9ca3af\",\n labelColor: \"#f9fafb\",\n crosshairColor: \"#6b7280\",\n regionFill: \"rgba(96, 165, 250, 0.15)\",\n regionStroke: \"rgba(96, 165, 250, 0.5)\",\n tooltipBg: \"#1f2937\",\n tooltipBorder: \"#4b5563\",\n tooltipText: \"#f9fafb\",\n};\n\n/**\n * Get the color for a spectrum at the given index.\n *\n * Cycles through the palette if index exceeds palette length.\n */\nexport function getSpectrumColor(index: number): string {\n return SPECTRUM_COLORS[index % SPECTRUM_COLORS.length];\n}\n\n/**\n * Get theme colors for the given theme name.\n */\nexport function getThemeColors(theme: \"light\" | \"dark\"): ThemeColors {\n return theme === \"dark\" ? DARK_THEME : LIGHT_THEME;\n}\n","/**\n * Hook for zoom and pan behavior backed by d3-zoom.\n *\n * Provides smooth mouse wheel zoom, click-drag pan, and double-click\n * reset. Works with both the SVG overlay and Canvas data layers.\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { zoom, type ZoomBehavior, zoomIdentity, type ZoomTransform } from \"d3-zoom\";\nimport { select } from \"d3-selection\";\nimport \"d3-transition\";\nimport type { ScaleLinear } from \"d3-scale\";\n\nexport interface ZoomPanState {\n /** Current d3 zoom transform. */\n transform: ZoomTransform;\n /** Whether the view is currently zoomed (not at identity). */\n isZoomed: boolean;\n}\n\nexport interface UseZoomPanOptions {\n /** Width of the plot area (excluding margins). */\n plotWidth: number;\n /** Height of the plot area (excluding margins). */\n plotHeight: number;\n /** Base x-scale (unzoomed). */\n xScale: ScaleLinear<number, number>;\n /** Base y-scale (unzoomed). */\n yScale: ScaleLinear<number, number>;\n /** Maximum zoom factor. */\n scaleExtent?: [number, number];\n /** Whether zoom/pan is enabled. */\n enabled?: boolean;\n /** Callback when the view changes. */\n onViewChange?: (xDomain: [number, number], yDomain: [number, number]) => void;\n}\n\nexport interface UseZoomPanReturn {\n /** Ref to attach to the interaction overlay element. */\n zoomRef: React.RefObject<SVGRectElement | null>;\n /** Current zoom/pan state. */\n state: ZoomPanState;\n /** Zoomed (rescaled) x-scale. */\n zoomedXScale: ScaleLinear<number, number>;\n /** Zoomed (rescaled) y-scale. */\n zoomedYScale: ScaleLinear<number, number>;\n /** Reset zoom to initial view. */\n resetZoom: () => void;\n /** Zoom in by a fixed step. */\n zoomIn: () => void;\n /** Zoom out by a fixed step. */\n zoomOut: () => void;\n}\n\n/** Zoom step multiplier for zoomIn/zoomOut. */\nconst ZOOM_STEP = 1.5;\n\nexport function useZoomPan(options: UseZoomPanOptions): UseZoomPanReturn {\n const {\n plotWidth,\n plotHeight,\n xScale,\n yScale,\n scaleExtent = [1, 50],\n enabled = true,\n onViewChange,\n } = options;\n\n const zoomRef = useRef<SVGRectElement | null>(null);\n const zoomBehaviorRef = useRef<ZoomBehavior<SVGRectElement, unknown> | null>(null);\n\n // Store callbacks and config in refs to avoid effect re-runs (REACT-2 fix)\n const onViewChangeRef = useRef(onViewChange);\n onViewChangeRef.current = onViewChange;\n const scaleExtentRef = useRef(scaleExtent);\n scaleExtentRef.current = scaleExtent;\n\n const [transform, setTransform] = useState<ZoomTransform>(zoomIdentity);\n\n // Memoize rescaled axes from the current transform (BUG-4 fix)\n const zoomedXScale = useMemo(\n () => transform.rescaleX(xScale.copy()),\n [transform, xScale],\n );\n const zoomedYScale = useMemo(\n () => transform.rescaleY(yScale.copy()),\n [transform, yScale],\n );\n\n // Set up d3-zoom behavior\n useEffect(() => {\n const element = zoomRef.current;\n if (!element || !enabled) return;\n\n const zoomBehavior = zoom<SVGRectElement, unknown>()\n .scaleExtent(scaleExtentRef.current)\n .extent([\n [0, 0],\n [plotWidth, plotHeight],\n ])\n .translateExtent([\n [-Infinity, -Infinity],\n [Infinity, Infinity],\n ])\n .on(\"zoom\", (event) => {\n const newTransform = event.transform as ZoomTransform;\n setTransform(newTransform);\n\n if (onViewChangeRef.current) {\n const newXScale = newTransform.rescaleX(xScale.copy());\n const newYScale = newTransform.rescaleY(yScale.copy());\n onViewChangeRef.current(\n newXScale.domain() as [number, number],\n newYScale.domain() as [number, number],\n );\n }\n });\n\n zoomBehaviorRef.current = zoomBehavior;\n\n select(element).call(zoomBehavior);\n\n // Double-click to reset\n select(element).on(\"dblclick.zoom\", () => {\n select(element).transition().duration(300).call(zoomBehavior.transform, zoomIdentity);\n });\n\n // Stale ref fix: use captured `element` instead of zoomRef.current\n return () => {\n select(element).on(\".zoom\", null);\n };\n }, [plotWidth, plotHeight, enabled, xScale, yScale]);\n\n const resetZoom = useCallback(() => {\n if (!zoomRef.current || !zoomBehaviorRef.current) return;\n select(zoomRef.current)\n .transition()\n .duration(300)\n .call(zoomBehaviorRef.current.transform, zoomIdentity);\n }, []);\n\n const zoomIn = useCallback(() => {\n if (!zoomRef.current || !zoomBehaviorRef.current) return;\n select(zoomRef.current)\n .transition()\n .duration(200)\n .call(zoomBehaviorRef.current.scaleBy, ZOOM_STEP);\n }, []);\n\n const zoomOut = useCallback(() => {\n if (!zoomRef.current || !zoomBehaviorRef.current) return;\n select(zoomRef.current)\n .transition()\n .duration(200)\n .call(zoomBehaviorRef.current.scaleBy, 1 / ZOOM_STEP);\n }, []);\n\n return {\n zoomRef,\n state: {\n transform,\n isZoomed: transform.k !== 1 || transform.x !== 0 || transform.y !== 0,\n },\n zoomedXScale,\n zoomedYScale,\n resetZoom,\n zoomIn,\n zoomOut,\n };\n}\n","/**\n * Canvas rendering layer for spectral data.\n *\n * Uses HTML5 Canvas 2D for high-performance rendering of spectral lines.\n * Redraws on zoom/pan transform changes and spectrum data changes.\n */\n\nimport { useEffect, useRef } from \"react\";\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { Spectrum } from \"../../types\";\nimport { drawAllSpectra } from \"../../utils/rendering\";\n\nexport interface SpectrumCanvasProps {\n /** Spectra to render. */\n spectra: Spectrum[];\n /** X-axis scale (already zoomed). */\n xScale: ScaleLinear<number, number>;\n /** Y-axis scale (already zoomed). */\n yScale: ScaleLinear<number, number>;\n /** Canvas width in pixels. */\n width: number;\n /** Canvas height in pixels. */\n height: number;\n /** ID of the currently highlighted spectrum. */\n highlightedId?: string;\n}\n\nexport function SpectrumCanvas({\n spectra,\n xScale,\n yScale,\n width,\n height,\n highlightedId,\n}: SpectrumCanvasProps) {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const dprRef = useRef(1);\n\n // Set up canvas DPR only when dimensions change (avoids flicker on zoom)\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const dpr = window.devicePixelRatio || 1;\n dprRef.current = dpr;\n canvas.width = width * dpr;\n canvas.height = height * dpr;\n }, [width, height]);\n\n // Redraw spectra when data or scales change\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return;\n\n const dpr = dprRef.current;\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\n drawAllSpectra(ctx, spectra, xScale, yScale, width, height, highlightedId);\n }, [spectra, xScale, yScale, width, height, highlightedId]);\n\n return (\n <canvas\n ref={canvasRef}\n style={{\n width,\n height,\n position: \"absolute\",\n top: 0,\n left: 0,\n pointerEvents: \"none\",\n }}\n />\n );\n}\n","/**\n * Canvas 2D rendering utilities for drawing spectral lines.\n *\n * Canvas is used for the data-heavy spectral line rendering (10K+ points)\n * while SVG (via visx) handles axes, annotations, and interactive overlays.\n */\n\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { Spectrum } from \"../types\";\nimport { getSpectrumColor } from \"./colors\";\n\n/** Line width for spectrum rendering. */\nconst LINE_WIDTH = 1.5;\n\n/** Line width when a spectrum is highlighted/hovered. */\nconst HIGHLIGHT_LINE_WIDTH = 2.5;\n\n/**\n * Clear the canvas and set up for drawing.\n */\nexport function clearCanvas(\n ctx: CanvasRenderingContext2D,\n width: number,\n height: number,\n): void {\n ctx.clearRect(0, 0, width, height);\n}\n\n/**\n * Draw a single spectrum as a line path on the canvas.\n *\n * Uses beginPath/lineTo for maximum performance with large point counts.\n * Skips points that fall outside the visible x-domain for zoom performance.\n */\nexport function drawSpectrum(\n ctx: CanvasRenderingContext2D,\n spectrum: Spectrum,\n index: number,\n xScale: ScaleLinear<number, number>,\n yScale: ScaleLinear<number, number>,\n options?: {\n highlighted?: boolean;\n opacity?: number;\n },\n): void {\n const { highlighted = false, opacity = 1.0 } = options ?? {};\n const n = Math.min(spectrum.x.length, spectrum.y.length);\n if (n < 2) return;\n\n const color = spectrum.color ?? getSpectrumColor(index);\n const lineWidth = highlighted ? HIGHLIGHT_LINE_WIDTH : LINE_WIDTH;\n\n // Get visible x-domain for culling\n const [xMin, xMax] = xScale.domain() as [number, number];\n const domainMin = Math.min(xMin, xMax);\n const domainMax = Math.max(xMin, xMax);\n\n ctx.save();\n ctx.beginPath();\n ctx.strokeStyle = color;\n ctx.lineWidth = lineWidth;\n ctx.globalAlpha = opacity;\n ctx.lineJoin = \"round\";\n\n let started = false;\n\n for (let i = 0; i < n; i++) {\n const xVal = spectrum.x[i] as number;\n\n // Skip points outside the visible domain (with 1-point margin for continuity)\n if (xVal < domainMin && i < n - 1 && (spectrum.x[i + 1] as number) < domainMin) {\n continue;\n }\n if (xVal > domainMax && i > 0 && (spectrum.x[i - 1] as number) > domainMax) {\n continue;\n }\n\n const px = xScale(xVal);\n const py = yScale(spectrum.y[i] as number);\n\n if (!started) {\n ctx.moveTo(px, py);\n started = true;\n } else {\n ctx.lineTo(px, py);\n }\n }\n\n ctx.stroke();\n ctx.restore();\n}\n\n/**\n * Draw all visible spectra onto the canvas.\n */\nexport function drawAllSpectra(\n ctx: CanvasRenderingContext2D,\n spectra: Spectrum[],\n xScale: ScaleLinear<number, number>,\n yScale: ScaleLinear<number, number>,\n width: number,\n height: number,\n highlightedId?: string,\n): void {\n clearCanvas(ctx, width, height);\n\n spectra.forEach((spectrum, index) => {\n if (spectrum.visible === false) return;\n\n drawSpectrum(ctx, spectrum, index, xScale, yScale, {\n highlighted: spectrum.id === highlightedId,\n opacity: highlightedId && spectrum.id !== highlightedId ? 0.3 : 1.0,\n });\n });\n}\n","/**\n * SVG axis layer rendering X and Y axes with labels and grid lines.\n *\n * Built with plain SVG for minimal bundle size. Handles reversed x-axis\n * (standard for IR wavenumber display).\n */\n\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { ThemeColors } from \"../../utils/colors\";\n\nexport interface AxisLayerProps {\n /** X-axis scale (already zoomed). */\n xScale: ScaleLinear<number, number>;\n /** Y-axis scale (already zoomed). */\n yScale: ScaleLinear<number, number>;\n /** Plot area width (excluding margins). */\n width: number;\n /** Plot area height (excluding margins). */\n height: number;\n /** X-axis label. */\n xLabel?: string;\n /** Y-axis label. */\n yLabel?: string;\n /** Show grid lines. */\n showGrid?: boolean;\n /** Theme colors. */\n colors: ThemeColors;\n}\n\n/** Number of tick marks to show on each axis. */\nconst TICK_COUNT = 8;\n\n/**\n * Generate evenly-spaced tick values for a linear scale.\n */\nfunction generateTicks(scale: ScaleLinear<number, number>, count: number): number[] {\n const [d0, d1] = scale.domain() as [number, number];\n const min = Math.min(d0, d1);\n const max = Math.max(d0, d1);\n const step = (max - min) / (count - 1);\n return Array.from({ length: count }, (_, i) => min + i * step);\n}\n\n/**\n * Format a tick value for display.\n */\nfunction formatTick(value: number): string {\n if (Math.abs(value) >= 1000) return Math.round(value).toString();\n if (Math.abs(value) >= 1) return value.toFixed(1);\n if (Math.abs(value) >= 0.01) return value.toFixed(3);\n return value.toExponential(1);\n}\n\nexport function AxisLayer({\n xScale,\n yScale,\n width,\n height,\n xLabel,\n yLabel,\n showGrid = true,\n colors,\n}: AxisLayerProps) {\n const xTicks = generateTicks(xScale, TICK_COUNT);\n const yTicks = generateTicks(yScale, TICK_COUNT - 2);\n\n return (\n <g>\n {/* Grid lines */}\n {showGrid && (\n <g>\n {xTicks.map((tick) => (\n <line\n key={`xgrid-${tick}`}\n x1={xScale(tick)}\n x2={xScale(tick)}\n y1={0}\n y2={height}\n stroke={colors.gridColor}\n strokeWidth={0.5}\n />\n ))}\n {yTicks.map((tick) => (\n <line\n key={`ygrid-${tick}`}\n x1={0}\n x2={width}\n y1={yScale(tick)}\n y2={yScale(tick)}\n stroke={colors.gridColor}\n strokeWidth={0.5}\n />\n ))}\n </g>\n )}\n\n {/* X-axis */}\n <g transform={`translate(0, ${height})`}>\n <line x1={0} x2={width} y1={0} y2={0} stroke={colors.axisColor} />\n {xTicks.map((tick) => (\n <g key={`xtick-${tick}`} transform={`translate(${xScale(tick)}, 0)`}>\n <line y1={0} y2={6} stroke={colors.axisColor} />\n <text\n y={20}\n textAnchor=\"middle\"\n fill={colors.tickColor}\n fontSize={11}\n fontFamily=\"system-ui, sans-serif\"\n >\n {formatTick(tick)}\n </text>\n </g>\n ))}\n {xLabel && (\n <text\n x={width / 2}\n y={42}\n textAnchor=\"middle\"\n fill={colors.labelColor}\n fontSize={13}\n fontFamily=\"system-ui, sans-serif\"\n >\n {xLabel}\n </text>\n )}\n </g>\n\n {/* Y-axis */}\n <g>\n <line x1={0} x2={0} y1={0} y2={height} stroke={colors.axisColor} />\n {yTicks.map((tick) => (\n <g key={`ytick-${tick}`} transform={`translate(0, ${yScale(tick)})`}>\n <line x1={-6} x2={0} stroke={colors.axisColor} />\n <text\n x={-10}\n textAnchor=\"end\"\n dominantBaseline=\"middle\"\n fill={colors.tickColor}\n fontSize={11}\n fontFamily=\"system-ui, sans-serif\"\n >\n {formatTick(tick)}\n </text>\n </g>\n ))}\n {yLabel && (\n <text\n transform={`translate(-50, ${height / 2}) rotate(-90)`}\n textAnchor=\"middle\"\n fill={colors.labelColor}\n fontSize={13}\n fontFamily=\"system-ui, sans-serif\"\n >\n {yLabel}\n </text>\n )}\n </g>\n </g>\n );\n}\n","/**\n * Peak annotation markers rendered as SVG overlays.\n *\n * Displays small triangles at peak positions with wavenumber labels.\n * Supports click interaction for peak selection.\n */\n\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { Peak } from \"../../types\";\nimport type { ThemeColors } from \"../../utils/colors\";\n\nexport interface PeakMarkersProps {\n /** Peaks to display. */\n peaks: Peak[];\n /** X-axis scale (zoomed). */\n xScale: ScaleLinear<number, number>;\n /** Y-axis scale (zoomed). */\n yScale: ScaleLinear<number, number>;\n /** Theme colors. */\n colors: ThemeColors;\n /** Callback when a peak is clicked. */\n onPeakClick?: (peak: Peak) => void;\n}\n\n/** Size of the peak marker triangle. */\nconst MARKER_SIZE = 5;\n\n/** Vertical offset for the label above the marker. */\nconst LABEL_OFFSET = 14;\n\nexport function PeakMarkers({\n peaks,\n xScale,\n yScale,\n colors,\n onPeakClick,\n}: PeakMarkersProps) {\n // Get visible domain to cull off-screen peaks\n const [xMin, xMax] = xScale.domain() as [number, number];\n const domainMin = Math.min(xMin, xMax);\n const domainMax = Math.max(xMin, xMax);\n\n const visiblePeaks = peaks.filter(\n (p) => p.x >= domainMin && p.x <= domainMax,\n );\n\n return (\n <g className=\"spectraview-peaks\">\n {visiblePeaks.map((peak, i) => {\n const px = xScale(peak.x);\n const py = yScale(peak.y);\n\n return (\n <g\n key={`peak-${peak.x}-${i}`}\n transform={`translate(${px}, ${py})`}\n style={{ cursor: onPeakClick ? \"pointer\" : \"default\" }}\n onClick={() => onPeakClick?.(peak)}\n >\n {/* Triangle marker pointing down */}\n <polygon\n points={`0,${-MARKER_SIZE} ${-MARKER_SIZE},${-MARKER_SIZE * 2.5} ${MARKER_SIZE},${-MARKER_SIZE * 2.5}`}\n fill={colors.labelColor}\n opacity={0.8}\n />\n {/* Wavenumber label */}\n {peak.label && (\n <text\n y={-MARKER_SIZE * 2.5 - LABEL_OFFSET}\n textAnchor=\"middle\"\n fill={colors.labelColor}\n fontSize={10}\n fontFamily=\"system-ui, sans-serif\"\n fontWeight={500}\n >\n {peak.label}\n </text>\n )}\n </g>\n );\n })}\n </g>\n );\n}\n","/**\n * Region selection overlay for click-drag x-axis region selection.\n *\n * Renders highlighted rectangular regions on the spectrum and\n * handles mouse interaction for creating new regions.\n */\n\nimport type { ScaleLinear } from \"d3-scale\";\nimport type { Region } from \"../../types\";\nimport type { ThemeColors } from \"../../utils/colors\";\n\nexport interface RegionSelectorProps {\n /** Existing regions to display. */\n regions: Region[];\n /** X-axis scale (zoomed). */\n xScale: ScaleLinear<number, number>;\n /** Plot area height. */\n height: number;\n /** Theme colors. */\n colors: ThemeColors;\n}\n\nexport function RegionSelector({\n regions,\n xScale,\n height,\n colors,\n}: RegionSelectorProps) {\n return (\n <g className=\"spectraview-regions\">\n {regions.map((region, i) => {\n const x1 = xScale(region.xStart);\n const x2 = xScale(region.xEnd);\n const left = Math.min(x1, x2);\n const w = Math.abs(x2 - x1);\n\n return (\n <g key={`region-${i}`}>\n <rect\n x={left}\n y={0}\n width={w}\n height={height}\n fill={region.color ?? colors.regionFill}\n stroke={colors.regionStroke}\n strokeWidth={1}\n />\n {region.label && (\n <text\n x={left + w / 2}\n y={12}\n textAnchor=\"middle\"\n fill={colors.labelColor}\n fontSize={10}\n fontFamily=\"system-ui, sans-serif\"\n >\n {region.label}\n </text>\n )}\n </g>\n );\n })}\n </g>\n );\n}\n","/**\n * Crosshair overlay showing current cursor position with coordinate readout.\n *\n * Pure rendering component — mouse tracking is handled by the parent\n * SpectraView component on the shared zoom/interaction rect.\n */\n\nimport type { ThemeColors } from \"../../utils/colors\";\n\n/** Position data for the crosshair. */\nexport interface CrosshairPosition {\n /** Pixel x coordinate within the plot area. */\n px: number;\n /** Pixel y coordinate within the plot area. */\n py: number;\n /** Data-space x value. */\n dataX: number;\n /** Data-space y value. */\n dataY: number;\n}\n\nexport interface CrosshairProps {\n /** Current crosshair position, or null when not hovering. */\n position: CrosshairPosition | null;\n /** Plot area width. */\n width: number;\n /** Plot area height. */\n height: number;\n /** Theme colors. */\n colors: ThemeColors;\n}\n\nexport function Crosshair({\n position,\n width,\n height,\n colors,\n}: CrosshairProps) {\n if (!position) return null;\n\n return (\n <g className=\"spectraview-crosshair\" pointerEvents=\"none\">\n {/* Vertical line */}\n <line\n x1={position.px}\n x2={position.px}\n y1={0}\n y2={height}\n stroke={colors.crosshairColor}\n strokeWidth={1}\n strokeDasharray=\"4 4\"\n />\n {/* Horizontal line */}\n <line\n x1={0}\n x2={width}\n y1={position.py}\n y2={position.py}\n stroke={colors.crosshairColor}\n strokeWidth={1}\n strokeDasharray=\"4 4\"\n />\n {/* Coordinate readout */}\n <g\n transform={`translate(${Math.min(position.px + 10, width - 100)}, ${Math.max(position.py - 10, 20)})`}\n >\n <rect\n x={0}\n y={-14}\n width={90}\n height={18}\n rx={3}\n fill={colors.tooltipBg}\n stroke={colors.tooltipBorder}\n strokeWidth={0.5}\n opacity={0.9}\n />\n <text\n x={5}\n y={0}\n fill={colors.tooltipText}\n fontSize={10}\n fontFamily=\"monospace\"\n >\n {formatValue(position.dataX)}, {formatValue(position.dataY)}\n </text>\n </g>\n </g>\n );\n}\n\nfunction formatValue(v: number): string {\n if (Math.abs(v) >= 100) return Math.round(v).toString();\n if (Math.abs(v) >= 1) return v.toFixed(1);\n return v.toFixed(4);\n}\n","/**\n * Toolbar component with zoom controls and action buttons.\n *\n * Provides zoom in, zoom out, reset, and export controls\n * for the SpectraView component.\n */\n\nimport type { Theme } from \"../../types\";\n\nexport interface ToolbarProps {\n /** Zoom in handler. */\n onZoomIn: () => void;\n /** Zoom out handler. */\n onZoomOut: () => void;\n /** Reset zoom handler. */\n onReset: () => void;\n /** Whether the view is currently zoomed. */\n isZoomed: boolean;\n /** Theme. */\n theme: Theme;\n}\n\n/** Inline styles to avoid CSS dependency for the toolbar. */\nconst buttonStyle = (theme: Theme): React.CSSProperties => ({\n display: \"inline-flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: 28,\n height: 28,\n border: `1px solid ${theme === \"dark\" ? \"#4b5563\" : \"#d1d5db\"}`,\n borderRadius: 4,\n background: theme === \"dark\" ? \"#1f2937\" : \"#ffffff\",\n color: theme === \"dark\" ? \"#d1d5db\" : \"#374151\",\n fontSize: 14,\n cursor: \"pointer\",\n padding: 0,\n lineHeight: 1,\n});\n\nconst toolbarStyle = (theme: Theme): React.CSSProperties => ({\n display: \"flex\",\n gap: 4,\n padding: \"4px 0\",\n borderBottom: `1px solid ${theme === \"dark\" ? \"#374151\" : \"#e5e7eb\"}`,\n});\n\nexport function Toolbar({\n onZoomIn,\n onZoomOut,\n onReset,\n isZoomed,\n theme,\n}: ToolbarProps) {\n return (\n <div style={toolbarStyle(theme)} className=\"spectraview-toolbar\">\n <button\n type=\"button\"\n style={buttonStyle(theme)}\n onClick={onZoomIn}\n title=\"Zoom in\"\n aria-label=\"Zoom in\"\n >\n +\n </button>\n <button\n type=\"button\"\n style={buttonStyle(theme)}\n onClick={onZoomOut}\n title=\"Zoom out\"\n aria-label=\"Zoom out\"\n >\n −\n </button>\n <button\n type=\"button\"\n style={{\n ...buttonStyle(theme),\n opacity: isZoomed ? 1 : 0.4,\n }}\n onClick={onReset}\n disabled={!isZoomed}\n title=\"Reset zoom\"\n aria-label=\"Reset zoom\"\n >\n ↺\n </button>\n </div>\n );\n}\n","/**\n * Hook for automatic peak detection in spectral data.\n *\n * Wraps the peak detection algorithm with React state management\n * and recalculates when spectra or options change.\n */\n\nimport { useMemo } from \"react\";\nimport type { Spectrum, Peak } from \"../types\";\nimport { detectPeaks, type PeakDetectionOptions } from \"../utils/peaks\";\n\nexport interface UsePeakPickingOptions extends PeakDetectionOptions {\n /** Whether peak picking is enabled. */\n enabled?: boolean;\n /** Only detect peaks for these spectrum IDs (all if not specified). */\n spectrumIds?: string[];\n}\n\n/**\n * Automatically detect peaks across one or more spectra.\n *\n * @param spectra - Array of spectra to analyze\n * @param options - Peak detection configuration\n * @returns Array of detected peaks with associated spectrum IDs\n */\nexport function usePeakPicking(\n spectra: Spectrum[],\n options: UsePeakPickingOptions = {},\n): Peak[] {\n const {\n enabled = true,\n spectrumIds,\n prominence,\n minDistance,\n maxPeaks,\n } = options;\n\n return useMemo(() => {\n if (!enabled) return [];\n\n const targetSpectra = spectrumIds\n ? spectra.filter((s) => spectrumIds.includes(s.id))\n : spectra;\n\n const allPeaks: Peak[] = [];\n\n for (const spectrum of targetSpectra) {\n if (spectrum.visible === false) continue;\n\n const peaks = detectPeaks(spectrum.x, spectrum.y, {\n prominence,\n minDistance,\n maxPeaks,\n });\n\n for (const peak of peaks) {\n allPeaks.push({\n ...peak,\n spectrumId: spectrum.id,\n });\n }\n }\n\n return allPeaks;\n }, [spectra, enabled, spectrumIds, prominence, minDistance, maxPeaks]);\n}\n","/**\n * Peak detection for spectral data.\n *\n * Uses a simple local-maxima algorithm with prominence filtering,\n * suitable for identifying peaks in IR, Raman, and NIR spectra.\n */\n\nimport type { Peak } from \"../types\";\n\n/** Default minimum prominence for peak detection. */\nconst DEFAULT_PROMINENCE = 0.01;\n\n/** Default minimum distance between peaks (in number of points). */\nconst DEFAULT_MIN_DISTANCE = 5;\n\nexport interface PeakDetectionOptions {\n /** Minimum prominence relative to the signal range. */\n prominence?: number;\n /** Minimum distance between peaks in data points. */\n minDistance?: number;\n /** Maximum number of peaks to return (sorted by prominence). */\n maxPeaks?: number;\n}\n\n/**\n * Detect peaks in a 1D signal using local maxima with prominence filtering.\n *\n * @param x - X-axis values (wavenumbers, wavelengths, etc.)\n * @param y - Y-axis values (absorbance, intensity, etc.)\n * @param options - Detection parameters\n * @returns Array of detected peaks sorted by x position\n */\nexport function detectPeaks(\n x: Float64Array | number[],\n y: Float64Array | number[],\n options: PeakDetectionOptions = {},\n): Peak[] {\n const {\n prominence = DEFAULT_PROMINENCE,\n minDistance = DEFAULT_MIN_DISTANCE,\n maxPeaks,\n } = options;\n\n if (x.length < 3 || y.length < 3) return [];\n\n // Find the signal range for prominence scaling\n let yMin = Infinity;\n let yMax = -Infinity;\n for (let i = 0; i < y.length; i++) {\n if (y[i] < yMin) yMin = y[i];\n if (y[i] > yMax) yMax = y[i];\n }\n const signalRange = yMax - yMin;\n if (signalRange === 0) return [];\n\n const absProminence = prominence * signalRange;\n\n // Find local maxima\n const candidates: { index: number; prom: number }[] = [];\n for (let i = 1; i < y.length - 1; i++) {\n if (y[i] > y[i - 1] && y[i] > y[i + 1]) {\n // Compute prominence: min drop to each side before a higher peak\n const leftMin = findMinBefore(y, i);\n const rightMin = findMinAfter(y, i);\n const prom = y[i] - Math.max(leftMin, rightMin);\n\n if (prom >= absProminence) {\n candidates.push({ index: i, prom });\n }\n }\n }\n\n // Sort by prominence (highest first) for distance filtering\n candidates.sort((a, b) => b.prom - a.prom);\n\n // Filter by minimum distance (keep most prominent first)\n const kept: { index: number; prom: number }[] = [];\n for (const c of candidates) {\n const tooClose = kept.some(\n (k) => Math.abs(k.index - c.index) < minDistance,\n );\n if (!tooClose) {\n kept.push(c);\n }\n }\n\n // Apply max peaks limit\n const selected = maxPeaks ? kept.slice(0, maxPeaks) : kept;\n\n // Convert to Peak objects, sorted by x position\n return selected\n .map((c) => ({\n x: x[c.index] as number,\n y: y[c.index] as number,\n label: formatWavenumber(x[c.index] as number),\n }))\n .sort((a, b) => a.x - b.x);\n}\n\n/**\n * Find the minimum value in y before index i, stopping at a higher peak.\n */\nfunction findMinBefore(y: Float64Array | number[], i: number): number {\n let min = y[i] as number;\n for (let j = i - 1; j >= 0; j--) {\n if (y[j] > y[i]) break;\n if ((y[j] as number) < min) min = y[j] as number;\n }\n return min;\n}\n\n/**\n * Find the minimum value in y after index i, stopping at a higher peak.\n */\nfunction findMinAfter(y: Float64Array | number[], i: number): number {\n let min = y[i] as number;\n for (let j = i + 1; j < y.length; j++) {\n if (y[j] > y[i]) break;\n if ((y[j] as number) < min) min = y[j] as number;\n }\n return min;\n}\n\n/**\n * Format a wavenumber value for display as a peak label.\n */\nfunction formatWavenumber(value: number): string {\n return Math.round(value).toString();\n}\n","/**\n * Hook for managing spectrum data loading and state.\n *\n * Handles file loading (drag-and-drop, file input), parsing,\n * and managing the collection of loaded spectra.\n */\n\nimport { useCallback, useState } from \"react\";\nimport type { Spectrum } from \"../types\";\nimport { parseCsv } from \"../parsers/csv\";\nimport { parseJson } from \"../parsers/json\";\nimport { parseJcamp } from \"../parsers/jcamp\";\n\nexport interface UseSpectrumDataReturn {\n /** Currently loaded spectra. */\n spectra: Spectrum[];\n /** Whether a file is currently being loaded. */\n loading: boolean;\n /** Last error message, if any. */\n error: string | null;\n /** Load spectra from a File object (detects format from extension). */\n loadFile: (file: File) => Promise<void>;\n /** Load spectra from a raw text string with explicit format. */\n loadText: (text: string, format: \"jcamp\" | \"csv\" | \"json\") => Promise<void>;\n /** Add a spectrum directly. */\n addSpectrum: (spectrum: Spectrum) => void;\n /** Remove a spectrum by ID. */\n removeSpectrum: (id: string) => void;\n /** Toggle a spectrum's visibility. */\n toggleVisibility: (id: string) => void;\n /** Clear all loaded spectra. */\n clear: () => void;\n}\n\n/**\n * Detect file format from file extension.\n */\nfunction detectFormat(filename: string): \"jcamp\" | \"csv\" | \"json\" | null {\n const ext = filename.toLowerCase().split(\".\").pop();\n switch (ext) {\n case \"dx\":\n case \"jdx\":\n case \"jcamp\":\n return \"jcamp\";\n case \"csv\":\n case \"tsv\":\n case \"txt\":\n return \"csv\";\n case \"json\":\n return \"json\";\n default:\n return null;\n }\n}\n\n/**\n * Hook for loading and managing spectral data.\n */\nexport function useSpectrumData(\n initialSpectra: Spectrum[] = [],\n): UseSpectrumDataReturn {\n const [spectra, setSpectra] = useState<Spectrum[]>(initialSpectra);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const loadText = useCallback(\n async (text: string, format: \"jcamp\" | \"csv\" | \"json\") => {\n setLoading(true);\n setError(null);\n\n try {\n let parsed: Spectrum[];\n\n switch (format) {\n case \"jcamp\":\n parsed = await parseJcamp(text);\n break;\n case \"csv\":\n parsed = [parseCsv(text)];\n break;\n case \"json\":\n parsed = parseJson(text);\n break;\n }\n\n setSpectra((prev) => [...prev, ...parsed]);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Failed to parse file\";\n setError(message);\n } finally {\n setLoading(false);\n }\n },\n [],\n );\n\n const loadFile = useCallback(\n async (file: File) => {\n const format = detectFormat(file.name);\n if (!format) {\n setError(`Unsupported file format: ${file.name}`);\n return;\n }\n\n const text = await file.text();\n await loadText(text, format);\n },\n [loadText],\n );\n\n const addSpectrum = useCallback((spectrum: Spectrum) => {\n setSpectra((prev) => [...prev, spectrum]);\n }, []);\n\n const removeSpectrum = useCallback((id: string) => {\n setSpectra((prev) => prev.filter((s) => s.id !== id));\n }, []);\n\n const toggleVisibility = useCallback((id: string) => {\n setSpectra((prev) =>\n prev.map((s) =>\n s.id === id ? { ...s, visible: s.visible === false ? true : false } : s,\n ),\n );\n }, []);\n\n const clear = useCallback(() => {\n setSpectra([]);\n setError(null);\n }, []);\n\n return {\n spectra,\n loading,\n error,\n loadFile,\n loadText,\n addSpectrum,\n removeSpectrum,\n toggleVisibility,\n clear,\n };\n}\n","/**\n * CSV/TSV parser for spectral data.\n *\n * Handles comma, tab, and semicolon delimiters with automatic detection.\n * Supports files with or without header rows.\n */\n\nimport type { Spectrum } from \"../types\";\n\nexport interface CsvParseOptions {\n /** Column delimiter (auto-detected if not provided). */\n delimiter?: string;\n /** Zero-based index of the x-value column. */\n xColumn?: number;\n /** Zero-based index of the y-value column. */\n yColumn?: number;\n /** Whether the first row is a header. */\n hasHeader?: boolean;\n /** Label for the parsed spectrum. */\n label?: string;\n}\n\n/** Delimiters to try during auto-detection. */\nconst DELIMITER_CANDIDATES = [\"\\t\", \",\", \";\", \" \"] as const;\n\n/**\n * Auto-detect the delimiter used in a CSV text.\n *\n * Counts occurrences of each candidate delimiter in the first 5 lines\n * and picks the one that appears most consistently.\n */\nfunction detectDelimiter(text: string): string {\n const lines = text.trim().split(/\\r?\\n/).slice(0, 5);\n let bestDelimiter = \",\";\n let bestScore = 0;\n\n for (const d of DELIMITER_CANDIDATES) {\n const counts = lines.map((line) => line.split(d).length - 1);\n const minCount = Math.min(...counts);\n // A good delimiter appears consistently across all lines\n if (minCount > 0 && minCount >= bestScore) {\n const consistent = counts.every((c) => c === counts[0]);\n if (consistent || minCount > bestScore) {\n bestScore = minCount;\n bestDelimiter = d;\n }\n }\n }\n\n return bestDelimiter;\n}\n\n/**\n * Parse a CSV/TSV string into a Spectrum object.\n *\n * @param text - Raw CSV/TSV text content\n * @param options - Parsing configuration\n * @returns Parsed Spectrum\n * @throws Error if the data cannot be parsed\n */\nexport function parseCsv(text: string, options: CsvParseOptions = {}): Spectrum {\n const {\n xColumn = 0,\n yColumn = 1,\n hasHeader = true,\n label = \"CSV Spectrum\",\n } = options;\n\n const delimiter = options.delimiter ?? detectDelimiter(text);\n const lines = text.trim().split(/\\r?\\n/);\n\n if (lines.length < 2) {\n throw new Error(\"CSV file must contain at least 2 lines\");\n }\n\n let headerLabel = label;\n let startIndex = 0;\n\n if (hasHeader) {\n const headers = lines[0].split(delimiter).map((h) => h.trim());\n // Only use header as label if no explicit label was provided\n if (!options.label && headers[yColumn]) {\n headerLabel = headers[yColumn];\n }\n startIndex = 1;\n }\n\n const xValues: number[] = [];\n const yValues: number[] = [];\n\n for (let i = startIndex; i < lines.length; i++) {\n const line = lines[i].trim();\n if (line === \"\" || line.startsWith(\"#\")) continue;\n\n const parts = line.split(delimiter);\n const xVal = parseFloat(parts[xColumn]);\n const yVal = parseFloat(parts[yColumn]);\n\n if (!isNaN(xVal) && !isNaN(yVal)) {\n xValues.push(xVal);\n yValues.push(yVal);\n }\n }\n\n if (xValues.length === 0) {\n throw new Error(\"No valid numeric data found in CSV\");\n }\n\n return {\n id: `csv-${Date.now()}`,\n label: headerLabel,\n x: new Float64Array(xValues),\n y: new Float64Array(yValues),\n };\n}\n\n/**\n * Parse a CSV string containing multiple y-columns into multiple spectra.\n *\n * The first column is treated as x-values, and each subsequent column\n * becomes a separate spectrum.\n */\nexport function parseCsvMulti(\n text: string,\n options: Omit<CsvParseOptions, \"xColumn\" | \"yColumn\"> = {},\n): Spectrum[] {\n const { hasHeader = true, label } = options;\n const delimiter = options.delimiter ?? detectDelimiter(text);\n const lines = text.trim().split(/\\r?\\n/);\n\n if (lines.length < 2) {\n throw new Error(\"CSV file must contain at least 2 lines\");\n }\n\n const firstDataLine = lines[hasHeader ? 1 : 0];\n const numColumns = firstDataLine.split(delimiter).length;\n\n if (numColumns < 2) {\n throw new Error(\"CSV must have at least 2 columns (x + y)\");\n }\n\n let headers: string[] | undefined;\n let startIndex = 0;\n\n if (hasHeader) {\n headers = lines[0].split(delimiter).map((h) => h.trim());\n startIndex = 1;\n }\n\n const xValues: number[] = [];\n const yArrays: number[][] = Array.from({ length: numColumns - 1 }, () => []);\n\n for (let i = startIndex; i < lines.length; i++) {\n const line = lines[i].trim();\n if (line === \"\" || line.startsWith(\"#\")) continue;\n\n const parts = line.split(delimiter);\n const xVal = parseFloat(parts[0]);\n if (isNaN(xVal)) continue;\n\n xValues.push(xVal);\n for (let col = 1; col < numColumns; col++) {\n const yVal = parseFloat(parts[col]);\n yArrays[col - 1].push(isNaN(yVal) ? 0 : yVal);\n }\n }\n\n const xArray = new Float64Array(xValues);\n\n return yArrays.map((yArr, i) => ({\n id: `csv-${Date.now()}-${i}`,\n label: label ?? headers?.[i + 1] ?? `Spectrum ${i + 1}`,\n x: xArray,\n y: new Float64Array(yArr),\n }));\n}\n","/**\n * JSON parser for spectral data.\n *\n * Supports multiple JSON formats commonly used for spectral data exchange.\n */\n\nimport type { Spectrum, SpectrumType } from \"../types\";\n\n/**\n * JSON spectrum format: object with x and y arrays.\n *\n * Accepts objects like:\n * ```json\n * {\n * \"label\": \"My Spectrum\",\n * \"x\": [4000, 3999, ...],\n * \"y\": [0.1, 0.12, ...],\n * \"xUnit\": \"cm⁻¹\",\n * \"yUnit\": \"Absorbance\"\n * }\n * ```\n *\n * Also accepts arrays of such objects for multi-spectrum data.\n */\ninterface JsonSpectrumInput {\n label?: string;\n title?: string;\n name?: string;\n x: number[];\n y: number[];\n wavenumbers?: number[];\n wavelengths?: number[];\n intensities?: number[];\n absorbance?: number[];\n xUnit?: string;\n yUnit?: string;\n type?: SpectrumType;\n meta?: Record<string, string | number>;\n}\n\n/**\n * Parse a JSON string into one or more Spectrum objects.\n *\n * Handles both single spectrum objects and arrays of spectra.\n * Supports flexible key names (x/wavenumbers, y/intensities, etc.).\n *\n * @param text - Raw JSON string\n * @returns Array of parsed Spectrum objects\n * @throws Error if the JSON cannot be parsed or has invalid structure\n */\nexport function parseJson(text: string): Spectrum[] {\n let data: unknown;\n try {\n data = JSON.parse(text);\n } catch {\n throw new Error(\"Invalid JSON: failed to parse input\");\n }\n\n if (Array.isArray(data)) {\n return data.map((item, i) => parseSingleJson(item as JsonSpectrumInput, i));\n }\n\n if (typeof data === \"object\" && data !== null) {\n // Check if it's a wrapper object with a \"spectra\" array\n const obj = data as Record<string, unknown>;\n if (Array.isArray(obj.spectra)) {\n return (obj.spectra as JsonSpectrumInput[]).map((item, i) =>\n parseSingleJson(item, i),\n );\n }\n return [parseSingleJson(data as JsonSpectrumInput, 0)];\n }\n\n throw new Error(\"Invalid JSON structure: expected an object or array\");\n}\n\n/**\n * Parse a single JSON object into a Spectrum.\n */\nfunction parseSingleJson(input: JsonSpectrumInput, index: number): Spectrum {\n // Resolve x values from various key names\n const xRaw = input.x ?? input.wavenumbers ?? input.wavelengths;\n if (!xRaw || !Array.isArray(xRaw)) {\n throw new Error(\n `Spectrum ${index}: missing x-axis data (expected \"x\", \"wavenumbers\", or \"wavelengths\")`,\n );\n }\n\n // Resolve y values from various key names\n const yRaw = input.y ?? input.intensities ?? input.absorbance;\n if (!yRaw || !Array.isArray(yRaw)) {\n throw new Error(\n `Spectrum ${index}: missing y-axis data (expected \"y\", \"intensities\", or \"absorbance\")`,\n );\n }\n\n if (xRaw.length !== yRaw.length) {\n throw new Error(\n `Spectrum ${index}: x and y arrays must have the same length (got ${xRaw.length} and ${yRaw.length})`,\n );\n }\n\n const label = input.label ?? input.title ?? input.name ?? `Spectrum ${index + 1}`;\n\n return {\n id: `json-${Date.now()}-${index}`,\n label,\n x: new Float64Array(xRaw),\n y: new Float64Array(yRaw),\n xUnit: input.xUnit,\n yUnit: input.yUnit,\n type: input.type,\n meta: input.meta,\n };\n}\n","/**\n * JCAMP-DX parser for spectral data.\n *\n * Wraps the `jcampconverter` npm package (optional peer dependency)\n * to parse .dx, .jdx, and .jcamp files into Spectrum objects.\n *\n * If jcampconverter is not installed, a lightweight built-in parser\n * handles basic AFFN (ASCII Free Format Numeric) JCAMP-DX files.\n */\n\nimport type { Spectrum, SpectrumType } from \"../types\";\n\n/**\n * Minimal shape of jcampconverter output (to avoid hard dependency).\n */\ninterface JcampResult {\n flatten: Array<{\n spectra: Array<{\n data: Array<{\n x: number[];\n y: number[];\n }>;\n }>;\n info: Record<string, string>;\n }>;\n}\n\n/** Cached reference to jcampconverter (lazy-loaded). */\nlet converterModule: { convert: (text: string, options?: object) => JcampResult } | null =\n null;\nlet converterChecked = false;\n\n/**\n * Attempt to dynamically import jcampconverter.\n *\n * Uses a variable to prevent bundlers from statically analyzing the import,\n * since jcampconverter is an optional peer dependency.\n */\nasync function getConverter(): Promise<typeof converterModule> {\n if (converterChecked) return converterModule;\n converterChecked = true;\n try {\n // Variable prevents Vite/Webpack static import analysis\n const pkg = \"jcampconverter\";\n converterModule = await import(/* @vite-ignore */ pkg);\n } catch {\n converterModule = null;\n }\n return converterModule;\n}\n\n/**\n * Infer spectrum type from JCAMP-DX header metadata.\n */\nfunction inferType(info: Record<string, string>): SpectrumType {\n const dataType = (info[\"DATA TYPE\"] ?? info[\"DATATYPE\"] ?? \"\").toLowerCase();\n if (dataType.includes(\"infrared\") || dataType.includes(\"ir\")) return \"IR\";\n if (dataType.includes(\"raman\")) return \"Raman\";\n if (dataType.includes(\"nir\") || dataType.includes(\"near\")) return \"NIR\";\n if (dataType.includes(\"uv\") || dataType.includes(\"vis\")) return \"UV-Vis\";\n if (dataType.includes(\"fluor\")) return \"fluorescence\";\n return \"other\";\n}\n\n/**\n * Parse a JCAMP-DX string into Spectrum objects.\n *\n * Uses jcampconverter if available, otherwise falls back to the built-in\n * parser for basic AFFN format files.\n *\n * @param text - Raw JCAMP-DX file content\n * @returns Array of parsed Spectrum objects\n */\nexport async function parseJcamp(text: string): Promise<Spectrum[]> {\n const converter = await getConverter();\n if (converter) {\n return parseWithConverter(text, converter);\n }\n return [parseBasicJcamp(text)];\n}\n\n/**\n * Parse using jcampconverter library.\n */\nfunction parseWithConverter(\n text: string,\n converter: NonNullable<typeof converterModule>,\n): Spectrum[] {\n const result = converter.convert(text, { keepRecordsRegExp: /.*/ });\n\n return result.flatten.map((entry, i) => {\n const firstSpectrum = entry.spectra?.[0]?.data?.[0];\n if (!firstSpectrum) {\n throw new Error(`JCAMP block ${i}: no spectral data found`);\n }\n\n return {\n id: `jcamp-${Date.now()}-${i}`,\n label: entry.info?.TITLE ?? `Spectrum ${i + 1}`,\n x: new Float64Array(firstSpectrum.x),\n y: new Float64Array(firstSpectrum.y),\n xUnit: entry.info?.XUNITS ?? \"cm⁻¹\",\n yUnit: entry.info?.YUNITS ?? \"Absorbance\",\n type: inferType(entry.info),\n meta: entry.info,\n };\n });\n}\n\n/**\n * Lightweight built-in JCAMP-DX parser for basic AFFN format.\n *\n * Handles the most common case: single-spectrum files with\n * XYDATA=(X++(Y..Y)) in ASCII free-format.\n *\n * This does NOT support compressed formats (SQZ, DIF, DIFDUP, NTUP)\n * or multi-block files. For full support, install jcampconverter.\n */\nfunction parseBasicJcamp(text: string): Spectrum {\n const lines = text.split(/\\r?\\n/);\n const info: Record<string, string> = {};\n const xValues: number[] = [];\n const yValues: number[] = [];\n\n let inData = false;\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Parse labeled data records (##KEY= value)\n if (trimmed.startsWith(\"##\")) {\n const match = trimmed.match(/^##(.+?)=\\s*(.*)$/);\n if (match) {\n const key = match[1].trim().toUpperCase();\n const value = match[2].trim();\n\n if (key === \"XYDATA\" || key === \"XYPOINTS\") {\n inData = true;\n continue;\n }\n if (key === \"END\") {\n inData = false;\n continue;\n }\n\n info[key] = value;\n }\n continue;\n }\n\n // Parse data lines\n if (inData && trimmed !== \"\") {\n const values = trimmed.split(/[\\s,]+/).map(Number);\n if (values.length >= 2 && !values.some(isNaN)) {\n // XYDATA: first value is X, rest are Y values\n const x0 = values[0];\n const firstX = parseFloat(info[\"FIRSTX\"] ?? \"0\");\n const lastX = parseFloat(info[\"LASTX\"] ?? \"0\");\n const npoints = parseInt(info[\"NPOINTS\"] ?? \"0\", 10);\n\n if (npoints > 0 && values.length === 2) {\n // Simple X,Y pair format\n xValues.push(values[0]);\n yValues.push(values[1]);\n } else if (values.length > 1) {\n // X++(Y..Y) format — X is first, rest are Y\n const deltaX =\n npoints > 1 ? (lastX - firstX) / (npoints - 1) : 0;\n for (let j = 1; j < values.length; j++) {\n xValues.push(x0 + (j - 1) * deltaX);\n yValues.push(values[j]);\n }\n }\n }\n }\n }\n\n if (xValues.length === 0) {\n throw new Error(\n \"Failed to parse JCAMP-DX: no data found. Install jcampconverter for full format support.\",\n );\n }\n\n return {\n id: `jcamp-${Date.now()}`,\n label: info[\"TITLE\"] ?? \"JCAMP Spectrum\",\n x: new Float64Array(xValues),\n y: new Float64Array(yValues),\n xUnit: info[\"XUNITS\"] ?? \"cm⁻¹\",\n yUnit: info[\"YUNITS\"] ?? \"Absorbance\",\n type: inferType(info),\n meta: info,\n };\n}\n","/**\n * Hook for exporting the spectrum view as PNG, SVG, or CSV data.\n */\n\nimport { useCallback } from \"react\";\nimport type { Spectrum } from \"../types\";\n\nexport interface UseExportReturn {\n /** Export the canvas as a PNG data URL. */\n exportPng: (canvas: HTMLCanvasElement, filename?: string) => void;\n /** Export visible spectra as CSV text. */\n exportCsv: (spectra: Spectrum[], filename?: string) => void;\n /** Export visible spectra as JSON. */\n exportJson: (spectra: Spectrum[], filename?: string) => void;\n}\n\n/**\n * Trigger a file download in the browser.\n */\nfunction downloadBlob(blob: Blob, filename: string): void {\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n}\n\n/**\n * Hook for exporting spectrum data and visualizations.\n */\nexport function useExport(): UseExportReturn {\n const exportPng = useCallback(\n (canvas: HTMLCanvasElement, filename = \"spectrum.png\") => {\n canvas.toBlob((blob) => {\n if (blob) downloadBlob(blob, filename);\n }, \"image/png\");\n },\n [],\n );\n\n const exportCsv = useCallback(\n (spectra: Spectrum[], filename = \"spectra.csv\") => {\n const visible = spectra.filter((s) => s.visible !== false);\n if (visible.length === 0) return;\n\n // Build CSV with shared x-axis or per-spectrum columns\n if (visible.length === 1) {\n const s = visible[0];\n const header = `${s.xUnit ?? \"x\"},${s.yUnit ?? \"y\"}\\n`;\n const rows = Array.from(s.x).map(\n (x, i) => `${x},${s.y[i]}`,\n );\n const csv = header + rows.join(\"\\n\");\n downloadBlob(new Blob([csv], { type: \"text/csv\" }), filename);\n } else {\n // Multi-spectrum: each gets its own x,y column pair\n const maxLen = Math.max(...visible.map((s) => s.x.length));\n const header = visible\n .map((s) => `${s.label}_x,${s.label}_y`)\n .join(\",\");\n const rows: string[] = [];\n for (let i = 0; i < maxLen; i++) {\n const values = visible.map((s) => {\n if (i < s.x.length) return `${s.x[i]},${s.y[i]}`;\n return \",\";\n });\n rows.push(values.join(\",\"));\n }\n const csv = header + \"\\n\" + rows.join(\"\\n\");\n downloadBlob(new Blob([csv], { type: \"text/csv\" }), filename);\n }\n },\n [],\n );\n\n const exportJson = useCallback(\n (spectra: Spectrum[], filename = \"spectra.json\") => {\n const visible = spectra.filter((s) => s.visible !== false);\n const output = visible.map((s) => ({\n label: s.label,\n x: Array.from(s.x),\n y: Array.from(s.y),\n xUnit: s.xUnit,\n yUnit: s.yUnit,\n type: s.type,\n }));\n const json = JSON.stringify(output, null, 2);\n downloadBlob(new Blob([json], { type: \"application/json\" }), filename);\n },\n [],\n );\n\n return { exportPng, exportCsv, exportJson };\n}\n"],"mappings":"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","names":["index_exports","__export","AxisLayer","Crosshair","DARK_THEME","LIGHT_THEME","PeakMarkers","RegionSelector","SPECTRUM_COLORS","SpectraView","SpectrumCanvas","Toolbar","computeXExtent","computeYExtent","createXScale","createYScale","detectPeaks","getSpectrumColor","getThemeColors","parseCsv","parseCsvMulti","parseJcamp","parseJson","useExport","usePeakPicking","useSpectrumData","useZoomPan","__toCommonJS","import_react","import_d3_scale","import_d3_array","Y_PADDING","computeXExtent","spectra","globalMin","globalMax","s","min","max","computeYExtent","pad","createXScale","domain","width","margin","reverseX","plotWidth","d","createYScale","height","plotHeight","SPECTRUM_COLORS","LIGHT_THEME","DARK_THEME","getSpectrumColor","index","getThemeColors","theme","import_react","import_d3_zoom","import_d3_selection","import_d3_transition","ZOOM_STEP","useZoomPan","options","plotWidth","plotHeight","xScale","yScale","scaleExtent","enabled","onViewChange","zoomRef","zoomBehaviorRef","onViewChangeRef","scaleExtentRef","transform","setTransform","zoomedXScale","zoomedYScale","element","zoomBehavior","event","newTransform","newXScale","newYScale","resetZoom","zoomIn","zoomOut","import_react","LINE_WIDTH","HIGHLIGHT_LINE_WIDTH","clearCanvas","ctx","width","height","drawSpectrum","spectrum","index","xScale","yScale","options","highlighted","opacity","n","color","getSpectrumColor","lineWidth","xMin","xMax","domainMin","domainMax","started","i","xVal","px","py","drawAllSpectra","spectra","highlightedId","import_jsx_runtime","SpectrumCanvas","spectra","xScale","yScale","width","height","highlightedId","canvasRef","dprRef","canvas","dpr","ctx","drawAllSpectra","import_jsx_runtime","generateTicks","scale","count","d0","d1","min","step","_","i","formatTick","value","AxisLayer","xScale","yScale","width","height","xLabel","yLabel","showGrid","colors","xTicks","yTicks","tick","import_jsx_runtime","PeakMarkers","peaks","xScale","yScale","colors","onPeakClick","xMin","xMax","domainMin","domainMax","visiblePeaks","p","peak","i","px","py","import_jsx_runtime","RegionSelector","regions","xScale","height","colors","region","x1","x2","left","w","import_jsx_runtime","Crosshair","position","width","height","colors","formatValue","v","import_jsx_runtime","buttonStyle","theme","toolbarStyle","Toolbar","onZoomIn","onZoomOut","onReset","isZoomed","import_jsx_runtime","DEFAULT_MARGIN","DEFAULT_WIDTH","DEFAULT_HEIGHT","resolveConfig","props","inferLabels","spectra","xLabel","yLabel","first","SpectraView","peaks","regions","onPeakClick","onViewChange","onCrosshairMove","clipId","config","width","height","margin","reverseX","theme","plotWidth","plotHeight","colors","getThemeColors","labels","xExtent","computeXExtent","yExtent","computeYExtent","baseXScale","createXScale","baseYScale","createYScale","onViewChangeRef","stableOnViewChange","xDomain","yDomain","zoomRef","zoomState","zoomedXScale","zoomedYScale","resetZoom","zoomIn","zoomOut","useZoomPan","crosshairPos","setCrosshairPos","onCrosshairMoveRef","handleMouseMove","event","rect","px","py","dataX","dataY","handleMouseLeave","toolbarHeight","Toolbar","SpectrumCanvas","AxisLayer","RegionSelector","PeakMarkers","Crosshair","import_react","detectPeaks","x","y","options","prominence","minDistance","maxPeaks","yMin","yMax","i","signalRange","absProminence","candidates","leftMin","findMinBefore","rightMin","findMinAfter","prom","a","b","kept","c","k","formatWavenumber","min","j","value","usePeakPicking","spectra","options","enabled","spectrumIds","prominence","minDistance","maxPeaks","targetSpectra","s","allPeaks","spectrum","peaks","detectPeaks","peak","import_react","DELIMITER_CANDIDATES","detectDelimiter","text","lines","bestDelimiter","bestScore","d","counts","line","minCount","c","parseCsv","options","xColumn","yColumn","hasHeader","label","delimiter","headerLabel","startIndex","headers","h","xValues","yValues","i","parts","xVal","yVal","parseCsvMulti","numColumns","yArrays","col","xArray","yArr","parseJson","text","data","item","i","parseSingleJson","obj","input","index","xRaw","yRaw","label","converterModule","converterChecked","getConverter","inferType","info","dataType","parseJcamp","text","converter","parseWithConverter","parseBasicJcamp","entry","i","firstSpectrum","lines","xValues","yValues","inData","line","trimmed","match","key","value","values","x0","firstX","lastX","npoints","deltaX","j","detectFormat","filename","useSpectrumData","initialSpectra","spectra","setSpectra","loading","setLoading","error","setError","loadText","text","format","parsed","parseJcamp","parseCsv","parseJson","prev","err","message","loadFile","file","addSpectrum","spectrum","removeSpectrum","id","s","toggleVisibility","clear","import_react","downloadBlob","blob","filename","url","a","useExport","exportPng","canvas","exportCsv","spectra","visible","s","header","rows","x","i","csv","maxLen","values","exportJson","output","json"]}