Skip to content

Commit f6fccd7

Browse files
feat: baseline correction preview
1 parent 4718c7c commit f6fccd7

2 files changed

Lines changed: 154 additions & 0 deletions

File tree

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import styled from '@emotion/styled';
2+
import type { Spectrum1D } from '@zakodium/nmrium-core';
3+
import type { DoubleArray } from 'cheminfo-types';
4+
import { xFindClosestIndex } from 'ml-spectra-processing';
5+
import { useRef, useState } from 'react';
6+
7+
import { isSpectrum1D } from '../../data/data1d/Spectrum1D/isSpectrum1D.ts';
8+
import { useChartData } from '../context/ChartContext.tsx';
9+
import { useScaleChecked } from '../context/ScaleContext.tsx';
10+
import { Anchor } from '../elements/Anchor.tsx';
11+
import useSpectrum from '../hooks/useSpectrum.ts';
12+
13+
interface AnchorData {
14+
x: number;
15+
id: string;
16+
}
17+
18+
interface AnchorsProps {
19+
spectrum: Spectrum1D;
20+
initialAnchors: AnchorData[];
21+
onAnchorsChange: (anchors: AnchorData[]) => void;
22+
}
23+
24+
const Container = styled.div`
25+
position: absolute;
26+
width: 100%;
27+
left: 0;
28+
top: 0;
29+
height: 100%;
30+
overflow: hidden;
31+
pointer-events: none;
32+
`;
33+
34+
function Anchors(props: AnchorsProps) {
35+
const containerRef = useRef<HTMLDivElement>(null);
36+
const { spectrum, initialAnchors, onAnchorsChange } = props;
37+
const [anchors, updateAnchors] = useState(initialAnchors);
38+
const { scaleX, scaleY } = useScaleChecked();
39+
40+
function handleDragMove(id: string, newX: number) {
41+
updateAnchors((prev) =>
42+
prev.map((a) => (a.id === id ? { ...a, x: scaleX().invert(newX) } : a)),
43+
);
44+
}
45+
46+
function handleDragEnd(id: string) {
47+
updateAnchors((prev) => {
48+
const next = prev.map((a) => {
49+
if (a.id !== id) return a;
50+
return a;
51+
});
52+
onAnchorsChange(next);
53+
return next;
54+
});
55+
}
56+
57+
function handleDelete(id: string) {
58+
updateAnchors((prev) => {
59+
const next = prev.filter((a) => a.id !== id);
60+
onAnchorsChange(next);
61+
return next;
62+
});
63+
}
64+
65+
return (
66+
<Container ref={containerRef}>
67+
{anchors.map((anchor) => {
68+
const { x: xPPM } = anchor;
69+
const x = scaleX()(xPPM);
70+
const yPPM = getMedianY(xPPM, spectrum);
71+
const y = scaleY(spectrum.id)(yPPM);
72+
73+
return (
74+
<Anchor
75+
key={anchor.id}
76+
position={{ x, y }}
77+
containerRef={containerRef}
78+
onDragMove={(x) => handleDragMove(anchor.id, x)}
79+
onDragEnd={() => handleDragEnd(anchor.id)}
80+
onDelete={() => handleDelete(anchor.id)}
81+
/>
82+
);
83+
})}
84+
</Container>
85+
);
86+
}
87+
88+
const INITIAL_ANCHORS = [
89+
{
90+
id: 'a1',
91+
x: 1,
92+
},
93+
{ id: 'a2', x: 5 },
94+
{ id: 'a3', x: 8 },
95+
{ id: 'a4', x: 7 },
96+
];
97+
98+
export function BaselinePreview() {
99+
const [globalAnchors, setGlobalAnchors] = useState(INITIAL_ANCHORS);
100+
const spectrum = useSpectrum();
101+
const {
102+
toolOptions: { selectedTool },
103+
} = useChartData();
104+
105+
function handleGlobalChange(updated: any) {
106+
setGlobalAnchors(updated);
107+
}
108+
109+
if (!isSpectrum1D(spectrum) || selectedTool !== 'baselineCorrection') return;
110+
/**
111+
* TODO: Apply the baseline correction on the fly and pass the anchors along with the newly processed spectrum,
112+
* where the y-value of each anchor is also calculated on the fly.
113+
* This removes the need to store the y-value in the filter anchors.
114+
* Preview the spectrum after applying the baseline correction method
115+
*/
116+
117+
return (
118+
<Anchors
119+
spectrum={spectrum}
120+
initialAnchors={globalAnchors}
121+
onAnchorsChange={handleGlobalChange}
122+
/>
123+
);
124+
}
125+
126+
function getMedianY(x: number, spectrum: Spectrum1D, windowSize = 20): number {
127+
const { x: xValues, re: yValues } = spectrum.data;
128+
129+
const centerIndex = xFindClosestIndex(xValues, x);
130+
const halfWindow = Math.floor(windowSize / 2);
131+
132+
const fromIndex = Math.max(0, centerIndex - halfWindow);
133+
const toIndex = Math.min(xValues.length, centerIndex + halfWindow + 1);
134+
135+
const yWindow = yValues.slice(fromIndex, toIndex);
136+
137+
if (yWindow.length === 0) return 0;
138+
139+
return computeMedian(yWindow);
140+
}
141+
142+
function computeMedian(values: DoubleArray): number {
143+
const sorted = values.toSorted((a, b) => a - b);
144+
const mid = Math.floor(sorted.length / 2);
145+
const isOdd = sorted.length % 2 !== 0;
146+
147+
if (isOdd) {
148+
return sorted[mid];
149+
}
150+
151+
return (sorted[mid - 1] + sorted[mid]) / 2;
152+
}

src/component/1d/Viewer1D.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useChartData } from '../context/ChartContext.js';
1010
import { ScaleProvider } from '../context/ScaleContext.js';
1111
import Spinner from '../loader/Spinner.js';
1212

13+
import { BaselinePreview } from './BaselinePreview.tsx';
1314
import { BrushTracker1D } from './BrushTracker1D.js';
1415
import FooterBanner from './FooterBanner.js';
1516
import { SVGContent1D } from './SVGContent1D.js';
@@ -61,6 +62,7 @@ function InnerViewer1D(props: InnerViewer1DProps) {
6162
</MouseTracker>
6263
</BrushTracker1D>
6364
)}
65+
<BaselinePreview />
6466
</div>
6567
</ViewerResponsiveWrapper>
6668
)}

0 commit comments

Comments
 (0)