diff --git a/src/component/1d-2d/components/axis_unit_picker.tsx b/src/component/1d-2d/components/axis_unit_picker.tsx index f404774d45..3bff807369 100644 --- a/src/component/1d-2d/components/axis_unit_picker.tsx +++ b/src/component/1d-2d/components/axis_unit_picker.tsx @@ -5,7 +5,6 @@ import { assert } from 'react-science/ui'; import type { ContextMenuItem } from '../../elements/ContextMenuBluePrint.tsx'; import { ContextMenu } from '../../elements/ContextMenuBluePrint.tsx'; -import useCheckExperimentalFeature from '../../hooks/useCheckExperimentalFeature.ts'; import { axisUnitToLabel } from '../../hooks/use_axis_unit.ts'; interface AxisUnitPickerProps { @@ -17,9 +16,6 @@ interface AxisUnitPickerProps { export function AxisUnitPicker(props: AxisUnitPickerProps) { const { unit, allowedUnits, onChange, children } = props; - const isExperimental = useCheckExperimentalFeature(); - - if (!isExperimental) return children; const options: ContextMenuItem[] = allowedUnits.map((allowedUnit) => ({ key: allowedUnit, diff --git a/src/component/1d/HorizontalAxis1D.tsx b/src/component/1d/HorizontalAxis1D.tsx index 003bcd6cde..efa58ee1b7 100644 --- a/src/component/1d/HorizontalAxis1D.tsx +++ b/src/component/1d/HorizontalAxis1D.tsx @@ -20,15 +20,14 @@ export function HorizontalAxis1D() { const isInset = useIsInset(); const isExportingProcessStart = useCheckExportStatus(); - const { unit, allowedUnits, setUnit } = useHorizontalAxisUnit(); + const { unit, allowedUnits, setUnit, domain } = useHorizontalAxisUnit(); const unitLabel = axisUnitToLabel[unit]; const refAxis = useRef(null); const scaler = useMemo(() => { - // TODO apply unit conversion - return scaleX(null); - }, [scaleX]); + return scaleX({ customDomain: domain }); + }, [domain, scaleX]); const { ticks, scale: ticksScale } = useLinearPrimaryTicks( scaler, 'horizontal', diff --git a/src/component/1d/Line.tsx b/src/component/1d/Line.tsx index fc658d233f..3a2ed3389a 100644 --- a/src/component/1d/Line.tsx +++ b/src/component/1d/Line.tsx @@ -29,7 +29,7 @@ function Line({ data, id, display, index }: LineProps) { const paths = useMemo(() => { const _scaleX = scaleX(); - const _scaleY = scaleY(id); + const _scaleY = scaleY({ spectrumId: id }); const pathBuilder = new PathBuilder(); if (data?.x && data?.y && _scaleX(0)) { diff --git a/src/component/1d/multiplicityTree/MultiplicityTree.tsx b/src/component/1d/multiplicityTree/MultiplicityTree.tsx index 2426799bff..d77a177b17 100644 --- a/src/component/1d/multiplicityTree/MultiplicityTree.tsx +++ b/src/component/1d/multiplicityTree/MultiplicityTree.tsx @@ -44,7 +44,7 @@ export default function MultiplicityTree(props: MultiplicityTreeProps) { if (!spectrum || !isSpectrum1D(spectrum)) return null; const { from, to } = range; const maxY = getMaxY(spectrum, { from, to }); - const startY = scaleY(spectrum.id)(maxY) - marginBottom; + const startY = scaleY({ spectrumId: spectrum.id })(maxY) - marginBottom; const tree = generateTreeNodes(range, spectrum); diff --git a/src/component/1d/peaks/PeakAnnotations.tsx b/src/component/1d/peaks/PeakAnnotations.tsx index 99fbba60e4..1136778326 100644 --- a/src/component/1d/peaks/PeakAnnotations.tsx +++ b/src/component/1d/peaks/PeakAnnotations.tsx @@ -178,7 +178,7 @@ function PeakAnnotation({ const { scaleX, scaleY } = useScaleChecked(); const sx = scaleX()(x); - const sy = scaleY(spectrumKey)(y) - 5; + const sy = scaleY({ spectrumId: spectrumKey })(y) - 5; const { y1, y2 } = getLineYCoordinates(sign, isOverlap); diff --git a/src/component/1d/peaks/PeakEditionManager.tsx b/src/component/1d/peaks/PeakEditionManager.tsx index ef0e2b36c1..1b1487f375 100644 --- a/src/component/1d/peaks/PeakEditionManager.tsx +++ b/src/component/1d/peaks/PeakEditionManager.tsx @@ -93,7 +93,7 @@ export function PeakEditionProvider({ children }: Required) { } y = py + dy; if (useScaleY) { - y = scaleY(spectrum?.id)(py) + dy; + y = scaleY({ spectrumId: spectrum?.id })(py) + dy; } if (x + InputDimension.width > width) { diff --git a/src/component/1d/peaks/usePeakShapesPath.ts b/src/component/1d/peaks/usePeakShapesPath.ts index 209c003329..530f7cad2f 100644 --- a/src/component/1d/peaks/usePeakShapesPath.ts +++ b/src/component/1d/peaks/usePeakShapesPath.ts @@ -53,7 +53,7 @@ export function usePeakShapesPath(spectrum: Spectrum1D) { break; } const _scaleX = scaleX(); - const _scaleY = scaleY(spectrum.id); + const _scaleY = scaleY({ spectrumId: spectrum.id }); const pathBuilder = new PathBuilder(); let fill = 'transparent'; diff --git a/src/component/1d/tool/PeakPointer.tsx b/src/component/1d/tool/PeakPointer.tsx index 2fe720b0fd..c9c2bbf354 100644 --- a/src/component/1d/tool/PeakPointer.tsx +++ b/src/component/1d/tool/PeakPointer.tsx @@ -65,7 +65,9 @@ function PeakPointer() { if (!closePeak) return null; const x = scaleX()(closePeak.x); - const y = scaleY(activeSpectrum.id)(closePeak.y) - spectrumIndex * shiftY; + const y = + scaleY({ spectrumId: activeSpectrum.id })(closePeak.y) - + spectrumIndex * shiftY; return (
get2DXScale({ width, margin, xDomain, mode }, reverse), - [margin, mode, reverse, width, xDomain], + () => + get2DXScale( + { width, margin, xDomain: customDomain ?? xDomain, mode }, + reverse, + ), + [customDomain, margin, mode, reverse, width, xDomain], ); } @@ -46,11 +56,21 @@ function get2DYScale(options: Scale2DYOptions, reverse = false) { ); } -function useScale2DY(reverse?: boolean) { +interface UseScale2DIndirectOptions { + reverse?: boolean; + customDomain?: number[]; +} +function useScale2DY(options?: UseScale2DIndirectOptions) { + const { reverse, customDomain } = options ?? {}; const { height, margin, yDomain } = useChartData(); + return useMemo( - () => get2DYScale({ height, margin, yDomain }, reverse), - [height, margin, reverse, yDomain], + () => + get2DYScale( + { height, margin, yDomain: customDomain ?? yDomain }, + reverse, + ), + [customDomain, height, margin, reverse, yDomain], ); } diff --git a/src/component/context/ScaleContext.tsx b/src/component/context/ScaleContext.tsx index c46bf753bc..705984f6a2 100644 --- a/src/component/context/ScaleContext.tsx +++ b/src/component/context/ScaleContext.tsx @@ -14,8 +14,12 @@ import { useVerticalAlign } from '../hooks/useVerticalAlign.js'; import { useChartData } from './ChartContext.js'; +export interface ScaleLinearNumberOptions { + spectrumId?: number | null | string; + customDomain?: number[]; +} type ScaleLinearNumberFunction = ( - spectrumId?: number | null | string, + options?: ScaleLinearNumberOptions, ) => ScaleLinear; interface ScaleState { @@ -59,7 +63,7 @@ export function ScaleProvider({ children }: Required) { const isInset = useIsInset(); const scaleX = useCallback( - (spectrumId = null) => { + (options) => { if (isInset) { return getInsetXScale({ width, @@ -69,28 +73,26 @@ export function ScaleProvider({ children }: Required) { }); } - return getXScale({ width, margin, xDomains, xDomain, mode }, spectrumId); + return getXScale({ ...options, width, margin, xDomains, xDomain, mode }); }, [isInset, margin, mode, width, xDomain, xDomains], ); const scaleY = useCallback( - (spectrumId = null) => { + (options) => { if (isInset) { return getInsetYScale({ height, yDomain, margin, spectraBottomMargin }); } - return getYScale( - { - height, - margin, - yDomains, - yDomain, - verticalAlign, - spectraBottomMargin, - }, - spectrumId, - ); + return getYScale({ + ...options, + height, + margin, + yDomains, + yDomain, + verticalAlign, + spectraBottomMargin, + }); }, [ spectraBottomMargin, diff --git a/src/component/hooks/use_axis_unit.ts b/src/component/hooks/use_axis_unit.ts index f046a632b3..e314833104 100644 --- a/src/component/hooks/use_axis_unit.ts +++ b/src/component/hooks/use_axis_unit.ts @@ -17,14 +17,19 @@ import { defaultAxisUnit2DFid, defaultAxisUnit2DFt, } from '@zakodium/nmrium-core'; +import { scaleLinear } from 'd3-scale'; import { useCallback, useMemo } from 'react'; +import { assert, assertUnreachable } from 'react-science/ui'; import { match } from 'ts-pattern'; import { isSpectrum2D } from '../../data/data2d/Spectrum2D/index.ts'; +import { useScale2DX, useScale2DY } from '../2d/utilities/scale.ts'; import { useChartData } from '../context/ChartContext.tsx'; import { useDispatch } from '../context/DispatchContext.tsx'; +import { useScaleChecked } from '../context/ScaleContext.tsx'; import { useActiveNucleusTab } from './useActiveNucleusTab.ts'; +import { useActiveSpectra } from './useActiveSpectra.ts'; import useSpectrum from './useSpectrum.ts'; import { useVisibleSpectra1D } from './use_visible_spectra_1d.ts'; @@ -42,17 +47,69 @@ function assertIn(value: V, values: T[]): asserts value is T { } export function useHorizontalAxisUnit() { + const { + originDomain: { xDomain: originXDomain }, + } = useChartData(); const spectra = useVisibleSpectra1D(); + const activeSpectra = useActiveSpectra(); + const firstActiveSpectrum = useMemo(() => { + const firstSelected = activeSpectra?.find((s) => s.selected); + return firstSelected + ? spectra.find((s) => s.id === firstSelected.id) + : spectra[0]; + }, [activeSpectra, spectra]); + const { scaleX } = useScaleChecked(); + const { nucleus, nucleusUnits } = useAxisUnit1D(); const dispatch = useDispatch(); - const mode: keyof Nucleus1DUnit['horizontal'] = spectra[0]?.info.isFt + const mode: keyof Nucleus1DUnit['horizontal'] = firstActiveSpectrum?.info.isFt ? 'ft' : 'fid'; const unit: AxisUnit1DFid | AxisUnit1DFt = nucleusUnits.horizontal[mode]; const allowedUnits: AxisUnit1DFid[] | AxisUnit1DFt[] = mode === 'ft' ? axisUnits1DFt : axisUnits1DFid; + const domain = useMemo(() => { + function getPtDomain() { + const maxPt = firstActiveSpectrum?.data.x.length ?? 0; + const ppmToPoint = scaleLinear(originXDomain, [0, maxPt]); + + return scaleX() + .domain() + .map((v) => ppmToPoint(v)); + } + + return match(mode) + .with('fid', () => + match(unit) + .with('s', () => undefined) + .with('pt', getPtDomain) + .with('hz', 'ppm', (unit) => { + assertUnreachable(unit as never); + }) + .exhaustive(), + ) + .with('ft', () => + match(unit) + .with('ppm', () => undefined) + .with('pt', getPtDomain) + .with('hz', () => { + if (!firstActiveSpectrum) return undefined; + + const scale = scaleX(); + return scale + .domain() + .map((v) => v * firstActiveSpectrum.info.originFrequency); + }) + .with('s', (unit) => { + assertUnreachable(unit as never); + }) + .exhaustive(), + ) + .exhaustive(); + }, [firstActiveSpectrum, mode, originXDomain, scaleX, unit]); + const setUnit = useCallback( (unit: AxisUnit) => { match(mode) @@ -75,13 +132,17 @@ export function useHorizontalAxisUnit() { [dispatch, mode, nucleus], ); - return { mode, unit, allowedUnits, setUnit }; + return { mode, unit, allowedUnits, setUnit, domain }; } export function useDirectAxisUnit() { + const { + originDomain: { xDomain: originXDomain }, + } = useChartData(); const { nucleus, units } = useAxisUnit2D(); const spectrum = useSpectrum(); const dispatch = useDispatch(); + const scaleX = useScale2DX(); return useMemo(() => { if (!spectrum) return; @@ -93,6 +154,42 @@ export function useDirectAxisUnit() { const allowedUnits: AxisUnit2DFid[] | AxisUnit2DFt[] = mode === 'ft' ? axisUnits2DFt : axisUnits2DFid; + // spectrum.info.spectrumSize = [nbColumns, nbRows]; + // in nmrium-core formatSpectrum2D + const directAxisIndex = 0; + function getPtDomain() { + assert(isSpectrum2D(spectrum)); + const originPtDomain = [0, spectrum.info.spectrumSize[directAxisIndex]]; + const ppmToPoint = scaleLinear(originXDomain, originPtDomain); + + return scaleX.domain().map((v) => ppmToPoint(v)); + } + + const domain = match(mode) + .with('fid', () => + match(unit) + .with('s', () => undefined) + .with('pt', getPtDomain) + .with('hz', 'ppm', (unit) => { + assertUnreachable(unit as never); + }) + .exhaustive(), + ) + .with('ft', () => + match(unit) + .with('ppm', () => undefined) + .with('pt', getPtDomain) + .with('hz', () => { + const frequency = spectrum.info.originFrequency[directAxisIndex]; + return scaleX.domain().map((v) => v * frequency); + }) + .with('s', (unit) => { + assertUnreachable(unit as never); + }) + .exhaustive(), + ) + .exhaustive(); + function setUnit(unit: AxisUnit) { match(mode) .with('fid', (mode) => { @@ -112,14 +209,18 @@ export function useDirectAxisUnit() { .exhaustive(); } - return { mode, unit, allowedUnits, setUnit }; - }, [dispatch, nucleus, spectrum, units.direct]); + return { mode, unit, allowedUnits, setUnit, domain }; + }, [dispatch, nucleus, originXDomain, scaleX, spectrum, units.direct]); } export function useIndirectAxisUnit() { + const { + originDomain: { yDomain: originYDomain }, + } = useChartData(); const { nucleus, units } = useAxisUnit2D(); const spectrum = useSpectrum(); const dispatch = useDispatch(); + const scaleY = useScale2DY(); return useMemo(() => { if (!spectrum) return; @@ -132,6 +233,42 @@ export function useIndirectAxisUnit() { const allowedUnits: AxisUnit2DFid[] | AxisUnit2DFt[] = mode === 'ft' ? axisUnits2DFt : axisUnits2DFid; + // spectrum.info.spectrumSize = [nbColumns, nbRows]; + // in nmrium-core formatSpectrum2D + const indirectAxisIndex = 1; + function getPtDomain() { + assert(isSpectrum2D(spectrum)); + const originPtDomain = [0, spectrum.info.spectrumSize[indirectAxisIndex]]; + const ppmToPoint = scaleLinear(originYDomain, originPtDomain); + + return scaleY.domain().map((v) => ppmToPoint(v)); + } + + const domain = match(mode) + .with('fid', () => + match(unit) + .with('s', () => undefined) + .with('pt', getPtDomain) + .with('hz', 'ppm', (unit) => { + assertUnreachable(unit as never); + }) + .exhaustive(), + ) + .with('ft', () => + match(unit) + .with('ppm', () => undefined) + .with('pt', getPtDomain) + .with('hz', () => { + const frequency = spectrum.info.originFrequency[indirectAxisIndex]; + return scaleY.domain().map((v) => v * frequency); + }) + .with('s', (unit) => { + assertUnreachable(unit as never); + }) + .exhaustive(), + ) + .exhaustive(); + function setUnit(unit: AxisUnit) { match(mode) .with('fid', (mode) => { @@ -151,8 +288,8 @@ export function useIndirectAxisUnit() { .exhaustive(); } - return { mode, unit, allowedUnits, setUnit }; - }, [dispatch, nucleus, spectrum, units.indirect]); + return { mode, unit, allowedUnits, setUnit, domain }; + }, [dispatch, nucleus, originYDomain, scaleY, spectrum, units.indirect]); } const defaultUnit1D: Nucleus1DUnit = {