diff --git a/package-lock.json b/package-lock.json index 4ca238d67f..c8beabc0d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,9 +17,9 @@ "@tanstack/react-form": "^1.33.0", "@tanstack/react-store": "^0.11.0", "@tanstack/react-table": "^8.21.3", - "@zakodium/nmr-types": "^0.5.12", - "@zakodium/nmrium-core": "^0.7.24", - "@zakodium/nmrium-core-plugins": "^0.7.32", + "@zakodium/nmr-types": "^0.5.14", + "@zakodium/nmrium-core": "^0.7.31", + "@zakodium/nmrium-core-plugins": "^0.7.40", "@zakodium/pdnd-esm": "^1.0.2", "@zip.js/zip.js": "^2.8.26", "cheminfo-font": "^1.27.0", @@ -108,7 +108,7 @@ "stylelint": "^17.13.0", "stylelint-config-standard": "^40.0.0", "typescript": "~6.0.3", - "vite": "^8.0.16", + "vite": "=8.0.16", "vitest": "^4.1.9" }, "peerDependencies": { @@ -4016,9 +4016,9 @@ } }, "node_modules/@zakodium/nmrium-core": { - "version": "0.7.30", - "resolved": "https://registry.npmjs.org/@zakodium/nmrium-core/-/nmrium-core-0.7.30.tgz", - "integrity": "sha512-uds3UysKrAgm38Yt2swNnt8nmf5SHqt1LuVnewwDTolztl3Lead0RtgOYsRjnL30k6gpvGrgaoENkIMxSY0/kQ==", + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@zakodium/nmrium-core/-/nmrium-core-0.7.31.tgz", + "integrity": "sha512-DVhxwBTY5ftXGJ+HahdDwRJFXfRHipVijf8QLOduIYnru+f+c+axYalCFjmE0rvKTgo7vPekBVpRdcvg8u4lrQ==", "license": "CC-BY-NC-SA-4.0", "dependencies": { "cheminfo-types": "^1.15.0", @@ -4034,24 +4034,24 @@ } }, "node_modules/@zakodium/nmrium-core-plugins": { - "version": "0.7.39", - "resolved": "https://registry.npmjs.org/@zakodium/nmrium-core-plugins/-/nmrium-core-plugins-0.7.39.tgz", - "integrity": "sha512-WFO8UmUB4ZGwO+eCTjC1RfUBeCtv+raQmsuVx6L7zf1/BuD0N3Lq0q+JpKkdn85sfFGckyejJr1yo2qd91EVMQ==", + "version": "0.7.40", + "resolved": "https://registry.npmjs.org/@zakodium/nmrium-core-plugins/-/nmrium-core-plugins-0.7.40.tgz", + "integrity": "sha512-ZepZd+0TPgwTpGbJPH99VSBFlNrhqDjtr4dcvYo5Gy+VDz0gf0DeSmjOCnHeFwkuPnTTG/YHWG8/6BSQHScyzw==", "license": "CC-BY-NC-SA-4.0", "dependencies": { "@date-fns/utc": "^2.1.1", - "@zakodium/nmrium-core": "^0.7.30", + "@zakodium/nmrium-core": "^0.7.31", "cheminfo-types": "^1.15.0", "convert-to-jcamp": "^7.0.0", "date-fns": "^4.1.0", "file-collection": "^6.6.1", "gyromagnetic-ratio": "^2.0.0", "is-any-array": "^3.0.0", - "jcampconverter": "^12.3.3", + "jcampconverter": "^12.4.0", "linear-sum-assignment": "^1.0.9", "lodash.merge": "^4.6.2", "ml-spectra-processing": "^14.28.1", - "nmr-processing": "^22.13.0", + "nmr-processing": "^22.14.0", "openchemlib": "^9.22.0", "openchemlib-utils": "^8.15.0", "sdf-parser": "^8.0.0", @@ -8482,9 +8482,9 @@ } }, "node_modules/jcampconverter": { - "version": "12.3.3", - "resolved": "https://registry.npmjs.org/jcampconverter/-/jcampconverter-12.3.3.tgz", - "integrity": "sha512-Ayu6hbf5AM1eahyMp0EmnuDQnG2KRKYODq1SE6ITwcv3no2YCvbeQOvoRqdxwhV3t11PtESr57jvWvfttagvdw==", + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/jcampconverter/-/jcampconverter-12.4.0.tgz", + "integrity": "sha512-7sIYUsBW2ZWST702MbvUqoh5lkB2ud/5sexzshvSzpBIgHEw/rO4TIzMQit2WloJbl0jVFh9xH4D8o2kALAMtw==", "license": "CC-BY-NC-SA-4.0", "dependencies": { "cheminfo-types": "^1.15.0", @@ -9739,9 +9739,9 @@ } }, "node_modules/nmr-processing": { - "version": "22.13.0", - "resolved": "https://registry.npmjs.org/nmr-processing/-/nmr-processing-22.13.0.tgz", - "integrity": "sha512-udS0fxLjJgojI382/6LZvBGLs+z+edoiULrqNjHYCQdFCgsEp6JYBNnHABY29myvXGN9ml17Clhc/qLaUJDe3w==", + "version": "22.14.0", + "resolved": "https://registry.npmjs.org/nmr-processing/-/nmr-processing-22.14.0.tgz", + "integrity": "sha512-MD5Y8aDqY5NRCAWf6cvnqpgVlVlIkbM1DZ5Zf+pyw411UolQ970mtRWxHTr0RUB8wsB57f+UXniZVKqtwANbiA==", "license": "CC-BY-NC-SA-4.0", "dependencies": { "binary-search": "^1.3.6", diff --git a/package.json b/package.json index 0c92d8a4f9..740bfea213 100644 --- a/package.json +++ b/package.json @@ -71,9 +71,9 @@ "@tanstack/react-form": "^1.33.0", "@tanstack/react-store": "^0.11.0", "@tanstack/react-table": "^8.21.3", - "@zakodium/nmr-types": "^0.5.12", - "@zakodium/nmrium-core": "^0.7.24", - "@zakodium/nmrium-core-plugins": "^0.7.32", + "@zakodium/nmr-types": "^0.5.14", + "@zakodium/nmrium-core": "^0.7.31", + "@zakodium/nmrium-core-plugins": "^0.7.40", "@zakodium/pdnd-esm": "^1.0.2", "@zip.js/zip.js": "^2.8.26", "cheminfo-font": "^1.27.0", diff --git a/src/component/1d/FloatingRanges.tsx b/src/component/1d/FloatingRanges.tsx deleted file mode 100644 index 601c651a1d..0000000000 --- a/src/component/1d/FloatingRanges.tsx +++ /dev/null @@ -1,378 +0,0 @@ -import styled from '@emotion/styled'; -import type { Ranges } from '@zakodium/nmr-types'; -import type { BoundingBox } from '@zakodium/nmrium-core'; -import { checkMultiplicity } from 'nmr-processing'; -import { memo, useEffect, useState } from 'react'; -import { BsArrowsMove } from 'react-icons/bs'; -import { FaTimes } from 'react-icons/fa'; -import { Rnd } from 'react-rnd'; - -import { isSpectrum1D } from '../../data/data1d/Spectrum1D/index.js'; -import { isSignalRange } from '../../data/utilities/RangeUtilities.js'; -import type { SVGTableColumn } from '../SVGTable.js'; -import { SVGTable } from '../SVGTable.js'; -import { useChartData } from '../context/ChartContext.js'; -import { useDispatch } from '../context/DispatchContext.js'; -import { useGlobal } from '../context/GlobalContext.js'; -import type { ActionsButtonsPopoverProps } from '../elements/ActionsButtonsPopover.js'; -import { ActionsButtonsPopover } from '../elements/ActionsButtonsPopover.js'; -import { useActiveNucleusTab } from '../hooks/useActiveNucleusTab.js'; -import { usePanelPreferences } from '../hooks/usePanelPreferences.js'; -import { useSVGUnitConverter } from '../hooks/useSVGUnitConverter.js'; -import useSpectraByActiveNucleus from '../hooks/useSpectraPerNucleus.js'; -import { useCheckExportStatus } from '../hooks/useViewportSize.js'; -import { extractChemicalElement } from '../utility/extractChemicalElement.js'; -import { formatNumber } from '../utility/formatNumber.js'; - -const ReactRnd = styled(Rnd)` - border: 1px solid transparent; - - :hover { - background-color: white; - border: 1px solid #ebecf1; - - button { - visibility: visible; - } - } -`; - -interface RangesTableProps { - ranges: Ranges['values']; -} - -interface RangeItem { - id: string; - delta: string; - multiplicity: string; - integration: string; - coupling: string; -} - -function useMapRanges(ranges: Ranges['values']) { - const output: RangeItem[] = []; - const activeTab = useActiveNucleusTab(); - const preferences = usePanelPreferences('ranges', activeTab); - for (const range of ranges) { - const { id, from, to, integration, signals = [] } = range; - const relativeFlag = isSignalRange(range); - const formattedValue = formatNumber( - integration, - preferences.relative.format, - ); - const integrationValue = relativeFlag - ? formattedValue - : `[ ${formattedValue} ]`; - - const rangeText = `${formatNumber(from, preferences.from.format)} - ${formatNumber( - to, - preferences.to.format, - )}`; - if (signals.length > 0) { - for (const signal of signals) { - const { multiplicity, delta, js = [] } = signal; - const coupling = js - .map((jsItem) => - !Number.isNaN(jsItem.coupling) - ? formatNumber(jsItem.coupling, preferences.coupling.format) - : '', - ) - .join(','); - const signalDelta = !checkMultiplicity(multiplicity, ['m']) - ? rangeText - : formatNumber(delta, preferences.deltaPPM.format); - output.push({ - id, - delta: signalDelta, - multiplicity, - integration: integrationValue, - coupling, - }); - } - } else { - output.push({ - id, - delta: rangeText, - multiplicity: 'm', - integration: integrationValue, - coupling: '', - }); - } - } - return output; -} -function InnerSVGRangesTable(props: RangesTableProps) { - const { ranges } = props; - const { - view: { - spectra: { activeTab }, - }, - } = useChartData(); - const data = useMapRanges(ranges); - - if (!data) return null; - - const element = extractChemicalElement(activeTab); - - const columns: Array> = [ - { - accessorKey: 'delta', - header: 'δ (ppm)', - width: 100, - rowSpanGroupKey: 'id', - headerTextProps: { fontWeight: 'bold' }, - cellBoxProps: { stroke: '#dedede', fill: 'white', fillOpacity: 0.8 }, - headerBoxProps: { stroke: '#dedede', fill: '#E5E8EB' }, - }, - { - accessorKey: 'integration', - header: `Rel. ${element}`, - width: 60, - rowSpanGroupKey: 'id', - headerTextProps: { fontWeight: 'bold' }, - cellBoxProps: { stroke: '#dedede', fill: 'white', fillOpacity: 0.8 }, - headerBoxProps: { stroke: '#dedede', fill: '#E5E8EB' }, - }, - { - accessorKey: 'multiplicity', - header: 'Mult.', - width: 60, - rowSpanGroupKey: 'id', - headerTextProps: { fontWeight: 'bold' }, - cellBoxProps: { stroke: '#dedede', fill: 'white', fillOpacity: 0.8 }, - headerBoxProps: { stroke: '#dedede', fill: '#E5E8EB' }, - }, - { - accessorKey: 'coupling', - header: 'J (Hz)', - width: 120, - headerTextProps: { fontWeight: 'bold' }, - cellBoxProps: { stroke: '#dedede', fill: 'white', fillOpacity: 0.8 }, - headerBoxProps: { stroke: '#dedede', fill: '#E5E8EB' }, - }, - ]; - - return data={data} columns={columns} />; -} - -const SVGRangesTable = memo(InnerSVGRangesTable); - -interface DraggablePublicationStringProps { - ranges: Ranges['values'] | undefined; - bonding: BoundingBox; - spectrumKey: string; -} - -function DraggableRanges(props: DraggablePublicationStringProps) { - const { ranges = [], bonding: externalBounding, spectrumKey } = props; - const dispatch = useDispatch(); - const { viewerRef } = useGlobal(); - const [bounding, setBounding] = useState(externalBounding); - const [isMoveActive, setIsMoveActive] = useState(false); - const { percentToPixel, pixelToPercent } = useSVGUnitConverter(); - const isExportProcessStart = useCheckExportStatus(); - - useEffect(() => { - setBounding({ ...externalBounding }); - }, [externalBounding]); - - function handleResize( - internalBounding: Pick, - ) { - const { width = 0, height = 0 } = convertToPixel(externalBounding); - internalBounding.width += width; - internalBounding.height += height; - setBounding((prevBounding) => ({ - ...prevBounding, - ...convertToPercent(internalBounding), - })); - } - - function handleDrag(internalBounding: Pick) { - setBounding((prevBounding) => ({ - ...prevBounding, - ...convertToPercent(internalBounding), - })); - } - function handleChangeInsetBounding(bounding: Partial) { - if ( - typeof bounding?.width === 'number' && - typeof bounding?.height === 'number' - ) { - const { width, height } = externalBounding; - bounding.width += width; - bounding.height += height; - } - - dispatch({ - type: 'CHANGE_RANGES_VIEW_FLOATING_BOX_BOUNDING', - payload: { - spectrumKey, - bounding: convertToPercent(bounding), - target: 'rangesBounding', - }, - }); - } - - function convertToPixel(bounding: Partial) { - const { x, y, height, width } = bounding; - const output: Partial = {}; - - if (x) { - output.x = percentToPixel(x, 'x'); - } - if (y) { - output.y = percentToPixel(y, 'y'); - } - if (width) { - output.width = width; - } - if (height) { - output.height = height; - } - - return output; - } - function convertToPercent(bounding: Partial) { - const { x, y, height, width } = bounding; - const output: Partial = {}; - - if (x) { - output.x = pixelToPercent(x, 'x'); - } - if (y) { - output.y = pixelToPercent(y, 'y'); - } - if (width) { - output.width = width; - } - if (height) { - output.height = height; - } - - return output; - } - - function handleRemove() { - dispatch({ - type: 'TOGGLE_RANGES_VIEW_PROPERTY', - payload: { key: 'showRanges', spectrumKey }, - }); - } - - const actionButtons: ActionsButtonsPopoverProps['buttons'] = [ - { - icon: , - - intent: 'none', - title: 'Move ranges table', - style: { cursor: 'move' }, - className: 'handle', - }, - { - icon: , - intent: 'danger', - title: 'Hide ranges table', - onClick: handleRemove, - }, - ]; - if (!viewerRef || ranges.length === 0) return null; - - const { x: xInPercent, y: yInPercent } = bounding; - - const x = percentToPixel(xInPercent, 'x'); - const y = percentToPixel(yInPercent, 'y'); - - if (isExportProcessStart) { - return ( - - - - ); - } - - return ( - setIsMoveActive(true)} - onResize={(e, dir, eRef, size, position) => - handleResize({ ...size, ...position }) - } - onResizeStop={(e, dir, eRef, size, position) => - handleChangeInsetBounding({ ...size, ...position }) - } - onDrag={(e, { x, y }) => { - handleDrag({ x, y }); - }} - onDragStop={(e, { x, y }) => { - handleChangeInsetBounding({ x, y }); - setIsMoveActive(false); - }} - resizeHandleWrapperStyle={{ backgroundColor: 'white' }} - > - - - - - ); -} - -function useSpectraRanges() { - const spectra = useSpectraByActiveNucleus(); - const output: Record = {}; - - for (const spectrum of spectra) { - if (!isSpectrum1D(spectrum)) { - continue; - } - const { id: spectrumKey, ranges } = spectrum; - - if (!Array.isArray(ranges?.values) || ranges.values.length === 0) { - continue; - } - - output[spectrumKey] = ranges.values; - } - - return output; -} - -export function FloatingRanges() { - const spectraRanges = useSpectraRanges(); - const { - view: { ranges }, - } = useChartData(); - - const options = Object.entries(ranges); - - return options.map(([spectrumKey, viewOptions]) => { - const { showRanges, rangesBounding } = viewOptions; - if (!showRanges) return null; - - return ( - - ); - }); -} diff --git a/src/component/1d/peaks/Peaks.tsx b/src/component/1d/peaks/Peaks.tsx index 49300d0029..64c89a9d95 100644 --- a/src/component/1d/peaks/Peaks.tsx +++ b/src/component/1d/peaks/Peaks.tsx @@ -188,9 +188,12 @@ export default function Peaks(props: PeaksProps) { const spectrum = useSpectrum(emptyData) as Spectrum1D; const peaksViewState = useActiveSpectrumPeaksViewState(); const rangesViewState = useActiveSpectrumRangesViewState(); + const { tablePreferences } = usePanelPreferences( + peaksSource === 'peaks' ? 'peaks' : 'ranges', + nucleus, + ); const { deltaPPM: { format: peakFormat } = { format: '0.0' } } = - usePanelPreferences(peaksSource === 'peaks' ? 'peaks' : 'ranges', nucleus); - + tablePreferences; const canDisplaySpectrumPeaks = !spectrum.display.isVisible || spectrum.info?.isFid; let mode: PeaksMode = 'spread'; diff --git a/src/component/1d/peaks/usePeakShapesPath.ts b/src/component/1d/peaks/usePeakShapesPath.ts index 6c935f8971..544fd5dcd6 100644 --- a/src/component/1d/peaks/usePeakShapesPath.ts +++ b/src/component/1d/peaks/usePeakShapesPath.ts @@ -29,7 +29,6 @@ export function usePeakShapesPath(spectrum: Spectrum1D) { const frequency = spectrum.info.originFrequency; let pathSeries: DataXY | null = null; - switch (target) { case 'peakShape': { const { peak } = options; diff --git a/src/component/1d/ranges/FloatingRangeTablePreferencesModal.tsx b/src/component/1d/ranges/FloatingRangeTablePreferencesModal.tsx new file mode 100644 index 0000000000..6bcf29de0c --- /dev/null +++ b/src/component/1d/ranges/FloatingRangeTablePreferencesModal.tsx @@ -0,0 +1,238 @@ +import { DialogBody, DialogFooter, MenuItem } from '@blueprintjs/core'; +import { MultiSelect } from '@blueprintjs/select'; +import type { SignalKind } from '@zakodium/nmr-types'; +import { useMemo } from 'react'; +import type { Control } from 'react-hook-form'; +import { useController, useForm } from 'react-hook-form'; +import { Button } from 'react-science/ui'; + +import type { SignalKindItem } from '../../../data/constants/signalsKinds.ts'; +import { SIGNAL_KINDS } from '../../../data/constants/signalsKinds.ts'; +import { usePreferences } from '../../context/PreferencesContext.js'; +import { fieldLabelStyle } from '../../elements/FormatField.tsx'; +import Label from '../../elements/Label.tsx'; +import { StandardDialog } from '../../elements/StandardDialog.tsx'; +import useNucleus from '../../hooks/useNucleus.js'; +import { usePanelPreferencesByNuclei } from '../../hooks/usePanelPreferences.js'; +import type { NucleusPreferenceField } from '../../panels/extra/preferences/NucleusPreferences.tsx'; +import { NucleusPreferences } from '../../panels/extra/preferences/NucleusPreferences.tsx'; +import { getUniqueNuclei } from '../../utility/getUniqueNuclei.js'; + +const formatFields: NucleusPreferenceField[] = [ + { + id: 1, + label: 'Serial number :', + checkFieldName: 'floatingTablePreferences.showSerialNumber', + hideFormatField: true, + }, + { + id: 2, + label: 'Assignment label :', + checkFieldName: 'floatingTablePreferences.showAssignmentLabel', + hideFormatField: true, + }, + { + id: 3, + label: 'From (ppm) :', + checkFieldName: 'floatingTablePreferences.from.show', + formatFieldName: 'floatingTablePreferences.from.format', + }, + { + id: 4, + label: 'To (ppm) :', + checkFieldName: 'floatingTablePreferences.to.show', + formatFieldName: 'floatingTablePreferences.to.format', + }, + { + id: 5, + label: 'Absolute integration :', + checkFieldName: 'floatingTablePreferences.absolute.show', + formatFieldName: 'floatingTablePreferences.absolute.format', + }, + { + id: 6, + label: 'Relative integration :', + checkFieldName: 'floatingTablePreferences.relative.show', + formatFieldName: 'floatingTablePreferences.relative.format', + }, + { + id: 7, + label: 'δ (ppm) :', + checkFieldName: 'floatingTablePreferences.deltaPPM.show', + formatFieldName: 'floatingTablePreferences.deltaPPM.format', + }, + { + id: 8, + label: 'δ (Hz) :', + checkFieldName: 'floatingTablePreferences.deltaHz.show', + formatFieldName: 'floatingTablePreferences.deltaHz.format', + }, + { + id: 9, + label: 'Coupling (Hz) :', + checkFieldName: 'floatingTablePreferences.coupling.show', + formatFieldName: 'floatingTablePreferences.coupling.format', + }, + { + id: 10, + label: 'Kind :', + checkFieldName: 'floatingTablePreferences.showKind', + hideFormatField: true, + }, + { + id: 11, + label: 'Multiplicity :', + checkFieldName: 'floatingTablePreferences.showMultiplicity', + hideFormatField: true, + }, + { + id: 12, + label: 'Assignment :', + checkFieldName: 'floatingTablePreferences.showAssignment', + hideFormatField: true, + }, +]; + +interface InnerFloatingRangeTablePreferencesModalProps { + onCloseDialog: () => void; +} + +interface FloatingRangeTablePreferencesModalProps extends InnerFloatingRangeTablePreferencesModalProps { + isOpen: boolean; +} + +export function FloatingRangeTablePreferencesModal( + props: FloatingRangeTablePreferencesModalProps, +) { + const { onCloseDialog, isOpen } = props; + + if (!isOpen) return; + + return ( + + + + ); +} + +function InnerFloatingRangeTablePreferencesModal( + props: InnerFloatingRangeTablePreferencesModalProps, +) { + const { onCloseDialog } = props; + const preferences = usePreferences(); + const nucleus = useNucleus(); + const nuclei = useMemo(() => getUniqueNuclei(nucleus), [nucleus]); + const preferencesByNuclei = usePanelPreferencesByNuclei('ranges', nuclei); + const { handleSubmit, control } = useForm({ + defaultValues: preferencesByNuclei, + }); + function saveHandler() { + void handleSubmit((values) => { + preferences.dispatch({ + type: 'SET_PANELS_PREFERENCES', + payload: { key: 'ranges', value: values }, + }); + onCloseDialog(); + })(); + } + + return ( + <> + + {nuclei?.map((n) => ( + ( + + )} + /> + ))} + + +
+ +
+
+ + ); +} + +interface SignalKindFilterProps { + control: Control; + nucleus: string; +} + +function SignalKindFilter({ control, nucleus }: SignalKindFilterProps) { + const { field } = useController({ + control, + name: `nuclei.${nucleus}.floatingTablePreferences.signalKinds`, + defaultValue: [], + }); + + const selectedKinds: SignalKind[] = field.value ?? []; + + function handleSelect(item: SignalKindItem) { + const already = selectedKinds.includes(item.value); + field.onChange( + already + ? selectedKinds.filter((k) => k !== item.value) + : [...selectedKinds, item.value], + ); + } + + function handleRemove(item: SignalKindItem, index: number) { + field.onChange(selectedKinds.filter((_, i) => i !== index)); + } + + function handleClear() { + field.onChange([]); + } + + const selectedItems = SIGNAL_KINDS.filter((k) => + selectedKinds.includes(k.value), + ); + + return ( +