From a17c1e50133248874f9ae04b77d95fc41dae32da Mon Sep 17 00:00:00 2001 From: jobo322 Date: Fri, 5 Jun 2026 22:42:34 -0500 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20enhance=20peak=20optimization=20pro?= =?UTF-8?q?cess=20with=20additional=20iterations=20and=20parameters=20chor?= =?UTF-8?q?e:=20increase=20the=20limit=20for=20peak=20optimization=20to=20?= =?UTF-8?q?15=20=E2=80=A8fix:=20do=20not=20overwrite=20fwhm=20with=20defau?= =?UTF-8?q?lt=20shape=20values?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit new version of ml-gsd contain the fix chore: update ml-gsd to 14.1.0 --- src/component/1d/peaks/usePeakShapesPath.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/component/1d/peaks/usePeakShapesPath.ts b/src/component/1d/peaks/usePeakShapesPath.ts index 6c935f897..18e9f71f0 100644 --- a/src/component/1d/peaks/usePeakShapesPath.ts +++ b/src/component/1d/peaks/usePeakShapesPath.ts @@ -9,13 +9,13 @@ import { PathBuilder } from '../../utility/PathBuilder.js'; type PeaksShapesOptions = | { - target: 'peakShape'; - peak: Peak1D; - } + target: 'peakShape'; + peak: Peak1D; + } | { - target: 'peaksSum'; - peaks: Peak1D[]; - }; + target: 'peaksSum'; + peaks: Peak1D[]; + }; export function usePeakShapesPath(spectrum: Spectrum1D) { const { scaleX, scaleY } = useScaleChecked(); @@ -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; From cca87ee252419323b28fe1070f6822de772fed7c Mon Sep 17 00:00:00 2001 From: hamed musallam Date: Thu, 18 Jun 2026 16:01:51 +0200 Subject: [PATCH 2/8] refactor: ranges and peaks table preferences --- src/component/1d/FloatingRanges.tsx | 12 +- src/component/1d/peaks/Peaks.tsx | 7 +- src/component/1d/ranges/Ranges.tsx | 4 +- src/component/modal/EditPeakShapeModal.tsx | 4 +- .../modal/editRange/EditRangeModal.tsx | 10 +- .../forms/components/NewSignalTab.tsx | 6 +- .../panels/PeaksPanel/PeaksPreferences.tsx | 36 +-- .../panels/PeaksPanel/PeaksTable.tsx | 22 +- .../panels/RangesPanel/RangesPreferences.tsx | 44 ++-- .../panels/RangesPanel/RangesTable.tsx | 32 +-- .../panels/RangesPanel/RangesTableRow.tsx | 46 ++-- .../panelsPreferencesDefaultValues.ts | 65 +++--- src/demo/views/SnapshotView.tsx | 215 ++++++++++++------ 13 files changed, 298 insertions(+), 205 deletions(-) diff --git a/src/component/1d/FloatingRanges.tsx b/src/component/1d/FloatingRanges.tsx index 601c651a1..f14ac92d9 100644 --- a/src/component/1d/FloatingRanges.tsx +++ b/src/component/1d/FloatingRanges.tsx @@ -52,21 +52,21 @@ interface RangeItem { function useMapRanges(ranges: Ranges['values']) { const output: RangeItem[] = []; const activeTab = useActiveNucleusTab(); - const preferences = usePanelPreferences('ranges', activeTab); + const { tablePreferences } = 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, + tablePreferences.relative.format, ); const integrationValue = relativeFlag ? formattedValue : `[ ${formattedValue} ]`; - const rangeText = `${formatNumber(from, preferences.from.format)} - ${formatNumber( + const rangeText = `${formatNumber(from, tablePreferences.from.format)} - ${formatNumber( to, - preferences.to.format, + tablePreferences.to.format, )}`; if (signals.length > 0) { for (const signal of signals) { @@ -74,13 +74,13 @@ function useMapRanges(ranges: Ranges['values']) { const coupling = js .map((jsItem) => !Number.isNaN(jsItem.coupling) - ? formatNumber(jsItem.coupling, preferences.coupling.format) + ? formatNumber(jsItem.coupling, tablePreferences.coupling.format) : '', ) .join(','); const signalDelta = !checkMultiplicity(multiplicity, ['m']) ? rangeText - : formatNumber(delta, preferences.deltaPPM.format); + : formatNumber(delta, tablePreferences.deltaPPM.format); output.push({ id, delta: signalDelta, diff --git a/src/component/1d/peaks/Peaks.tsx b/src/component/1d/peaks/Peaks.tsx index 49300d002..64c89a9d9 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/ranges/Ranges.tsx b/src/component/1d/ranges/Ranges.tsx index 3f9258c68..6a7e2f8cf 100644 --- a/src/component/1d/ranges/Ranges.tsx +++ b/src/component/1d/ranges/Ranges.tsx @@ -100,7 +100,7 @@ export default function Ranges() { toolOptions: { selectedTool }, } = useChartData(); const spectrum = useSpectrum(emptyData) as Spectrum1D; - const rangesPreferences = usePanelPreferences('ranges', activeTab); + const { tablePreferences } = usePanelPreferences('ranges', activeTab); if ( !spectrum.ranges?.values || @@ -114,7 +114,7 @@ export default function Ranges() { ); } diff --git a/src/component/modal/EditPeakShapeModal.tsx b/src/component/modal/EditPeakShapeModal.tsx index d0fd7e9ab..7f8f31969 100644 --- a/src/component/modal/EditPeakShapeModal.tsx +++ b/src/component/modal/EditPeakShapeModal.tsx @@ -97,7 +97,7 @@ function InnerEditPeakShapeModal(props: Required) { const { peak, onCloseDialog } = props; const dispatch = useDispatch(); const activeTab = useActiveNucleusTab(); - const peaksPreferences = usePanelPreferences('peaks', activeTab); + const { tablePreferences } = usePanelPreferences('peaks', activeTab); const [kind, setKind] = useState(peak.shape?.kind || 'gaussian'); const { handleSubmit, control, reset } = useForm({ @@ -125,7 +125,7 @@ function InnerEditPeakShapeModal(props: Required) { setKind(value); } - const valuePPM = formatNumber(peak.x, peaksPreferences.deltaPPM.format); + const valuePPM = formatNumber(peak.x, tablePreferences.deltaPPM.format); return ( diff --git a/src/component/modal/editRange/forms/components/NewSignalTab.tsx b/src/component/modal/editRange/forms/components/NewSignalTab.tsx index acecf2746..3d728c32b 100644 --- a/src/component/modal/editRange/forms/components/NewSignalTab.tsx +++ b/src/component/modal/editRange/forms/components/NewSignalTab.tsx @@ -42,7 +42,7 @@ export function NewSignalTab(props: NewSignalTabProps) { const { selectedTabId: signalIndex, selectTab } = useTabsController(); const { signals } = useWatch(); const activeTab = useActiveNucleusTab(); - const rangesPreferences = usePanelPreferences('ranges', activeTab); + const { tablePreferences } = usePanelPreferences('ranges', activeTab); function saveHandler(val: any) { const newSignal = { @@ -95,8 +95,8 @@ export function NewSignalTab(props: NewSignalTabProps) { Edit or select a delta value of new signal in range [ {`${formatNumber( range.from, - rangesPreferences.from.format, - )} ppm - ${formatNumber(range.to, rangesPreferences.to.format)} ppm`} + tablePreferences.from.format, + )} ppm - ${formatNumber(range.to, tablePreferences.to.format)} ppm`} ]:

(); const deletePeakHandler = useCallback( @@ -64,7 +64,7 @@ export function usePeaksTableColumns(activeTab: string) { saveDeltaPPMRefsHandler(value, row.original)} type="number" @@ -78,7 +78,7 @@ export function usePeaksTableColumns(activeTab: string) { Header: 'δ (Hz)', accessor: 'xHz', Cell: ({ row }: CellProps) => - formatNumber(row.original.xHz, peaksPreferences.deltaHz.format), + formatNumber(row.original.xHz, tablePreferences.deltaHz.format), }, { showWhen: 'intensity.show', @@ -87,7 +87,7 @@ export function usePeaksTableColumns(activeTab: string) { style: { maxWidth: '80px' }, accessor: 'y', Cell: ({ row }: CellProps) => - formatNumber(row.original.y, peaksPreferences.intensity.format), + formatNumber(row.original.y, tablePreferences.intensity.format), }, { showWhen: 'peakWidth.show', @@ -95,7 +95,7 @@ export function usePeaksTableColumns(activeTab: string) { Header: 'Width (Hz)', accessor: 'width', Cell: ({ row }: CellProps) => - formatNumber(row.original.width, peaksPreferences.peakWidth.format), + formatNumber(row.original.width, tablePreferences.peakWidth.format), }, { showWhen: 'showKind', @@ -111,7 +111,7 @@ export function usePeaksTableColumns(activeTab: string) { Cell: ({ row }: CellProps) => { const fwhm = row.original?.shape?.fwhm; if (fwhm) { - return formatNumber(fwhm, peaksPreferences.fwhm.format); + return formatNumber(fwhm, tablePreferences.fwhm.format); } return ''; }, @@ -127,7 +127,7 @@ export function usePeaksTableColumns(activeTab: string) { row.original?.shape?.kind === 'pseudoVoigt' && row.original?.shape?.mu; if (mu) { - return formatNumber(mu, peaksPreferences.mu.format); + return formatNumber(mu, tablePreferences.mu.format); } return ''; }, @@ -144,7 +144,7 @@ export function usePeaksTableColumns(activeTab: string) { row.original?.shape?.kind === 'generalizedLorentzian' && row.original?.shape?.gamma; if (gamma) { - return formatNumber(gamma, peaksPreferences.gamma.format); + return formatNumber(gamma, tablePreferences.gamma.format); } return ''; }, @@ -170,7 +170,7 @@ export function usePeaksTableColumns(activeTab: string) { }, ], [ - peaksPreferences, + tablePreferences, saveDeltaPPMRefsHandler, deletePeakHandler, editPeakHandler, @@ -181,14 +181,14 @@ export function usePeaksTableColumns(activeTab: string) { const columns: Array> = []; for (const col of COLUMNS) { const { showWhen, ...colParams } = col; - if (dlv(peaksPreferences, showWhen)) { + if (dlv(tablePreferences, showWhen)) { addCustomColumn(columns, colParams); } } columns.sort((object1, object2) => object1.index - object2.index); return columns; - }, [COLUMNS, peaksPreferences]); + }, [COLUMNS, tablePreferences]); return { tableColumns, peak, setEditedPeak }; } diff --git a/src/component/panels/RangesPanel/RangesPreferences.tsx b/src/component/panels/RangesPanel/RangesPreferences.tsx index 85e2c09a5..90f80dca1 100644 --- a/src/component/panels/RangesPanel/RangesPreferences.tsx +++ b/src/component/panels/RangesPanel/RangesPreferences.tsx @@ -18,91 +18,91 @@ const formatFields: NucleusPreferenceField[] = [ { id: 1, label: 'Serial number :', - checkFieldName: 'showSerialNumber', + checkFieldName: 'tablePreferences.showSerialNumber', hideFormatField: true, }, { id: 2, label: 'Assignment label :', - checkFieldName: 'showAssignmentLabel', + checkFieldName: 'tablePreferences.showAssignmentLabel', hideFormatField: true, }, { id: 3, label: 'From (ppm) :', - checkFieldName: 'from.show', - formatFieldName: 'from.format', + checkFieldName: 'tablePreferences.from.show', + formatFieldName: 'tablePreferences.from.format', }, { id: 4, label: 'To (ppm) :', - checkFieldName: 'to.show', - formatFieldName: 'to.format', + checkFieldName: 'tablePreferences.to.show', + formatFieldName: 'tablePreferences.to.format', }, { id: 5, label: 'Absolute integration :', - checkFieldName: 'absolute.show', - formatFieldName: 'absolute.format', + checkFieldName: 'tablePreferences.absolute.show', + formatFieldName: 'tablePreferences.absolute.format', }, { id: 6, label: 'Relative integration :', - checkFieldName: 'relative.show', - formatFieldName: 'relative.format', + checkFieldName: 'tablePreferences.relative.show', + formatFieldName: 'tablePreferences.relative.format', }, { id: 7, label: 'δ (ppm) :', - checkFieldName: 'deltaPPM.show', - formatFieldName: 'deltaPPM.format', + checkFieldName: 'tablePreferences.deltaPPM.show', + formatFieldName: 'tablePreferences.deltaPPM.format', }, { id: 8, label: 'δ (Hz) :', - checkFieldName: 'deltaHz.show', - formatFieldName: 'deltaHz.format', + checkFieldName: 'tablePreferences.deltaHz.show', + formatFieldName: 'tablePreferences.deltaHz.format', }, { id: 9, label: 'Coupling (Hz) :', - checkFieldName: 'coupling.show', - formatFieldName: 'coupling.format', + checkFieldName: 'tablePreferences.coupling.show', + formatFieldName: 'tablePreferences.coupling.format', }, { id: 10, label: 'Kind :', - checkFieldName: 'showKind', + checkFieldName: 'tablePreferences.showKind', hideFormatField: true, }, { id: 11, label: 'Multiplicity :', - checkFieldName: 'showMultiplicity', + checkFieldName: 'tablePreferences.showMultiplicity', hideFormatField: true, }, { id: 12, label: 'Assignment links :', - checkFieldName: 'showAssignment', + checkFieldName: 'tablePreferences.showAssignment', hideFormatField: true, }, { id: 13, label: 'Delete action :', - checkFieldName: 'showDeleteAction', + checkFieldName: 'tablePreferences.showDeleteAction', hideFormatField: true, }, { id: 14, label: 'Zoom action :', - checkFieldName: 'showZoomAction', + checkFieldName: 'tablePreferences.showZoomAction', hideFormatField: true, }, { id: 15, label: 'Edit action :', - checkFieldName: 'showEditAction', + checkFieldName: 'tablePreferences.showEditAction', hideFormatField: true, }, ]; diff --git a/src/component/panels/RangesPanel/RangesTable.tsx b/src/component/panels/RangesPanel/RangesTable.tsx index 25a43bcc2..022ed517d 100644 --- a/src/component/panels/RangesPanel/RangesTable.tsx +++ b/src/component/panels/RangesPanel/RangesTable.tsx @@ -79,6 +79,8 @@ export default function RangesTable(props: RangesTableProps) { info, } = props; + const { tablePreferences } = preferences; + const element = extractChemicalElement(activeTab); const { items: sortedData, isSortedDesc, onSort } = useTableSortBy(tableData); const data = useMapRanges(sortedData); @@ -92,9 +94,9 @@ export default function RangesTable(props: RangesTableProps) { } const showActions = - preferences.showDeleteAction || - preferences.showEditAction || - preferences.showZoomAction; + tablePreferences.showDeleteAction || + tablePreferences.showEditAction || + tablePreferences.showZoomAction; return ( <> @@ -102,43 +104,43 @@ export default function RangesTable(props: RangesTableProps) { - {preferences.showSerialNumber && } - {preferences.showAssignmentLabel && ( + {tablePreferences.showSerialNumber && } + {tablePreferences.showAssignmentLabel && ( )} - {preferences.from.show && ( + {tablePreferences.from.show && ( )} - {preferences.to.show && ( + {tablePreferences.to.show && ( )} - {preferences.deltaPPM.show && ( + {tablePreferences.deltaPPM.show && ( )} - {preferences.deltaHz.show && } + {tablePreferences.deltaHz.show && } - {preferences.relative.show && ( + {tablePreferences.relative.show && ( )} - {preferences.absolute.show && } - {preferences.showMultiplicity && } - {preferences.coupling.show && } + {tablePreferences.absolute.show && } + {tablePreferences.showMultiplicity && } + {tablePreferences.coupling.show && } - {preferences.showAssignment && ( + {tablePreferences.showAssignment && ( )} - {preferences.showKind && } + {tablePreferences.showKind && } {showActions && } diff --git a/src/component/panels/RangesPanel/RangesTableRow.tsx b/src/component/panels/RangesPanel/RangesTableRow.tsx index f11ac95a5..01471d5a0 100644 --- a/src/component/panels/RangesPanel/RangesTableRow.tsx +++ b/src/component/panels/RangesPanel/RangesTableRow.tsx @@ -67,7 +67,7 @@ export default function RangesTableRow(props: RangesTableRowProps) { rowData, onContextMenuSelect, contextMenu = [], - preferences, + preferences: { tablePreferences }, info, } = props; const dispatch = useDispatch(); @@ -181,12 +181,12 @@ export default function RangesTableRow(props: RangesTableRowProps) { as="tr" style={trStyle} > - {preferences.showSerialNumber && ( + {tablePreferences.showSerialNumber && ( )} - {preferences.showAssignmentLabel && ( + {tablePreferences.showAssignmentLabel && ( )} - {preferences.from.show && ( + {tablePreferences.from.show && ( )} - {preferences.to.show && ( + {tablePreferences.to.show && ( )} - {preferences.deltaPPM.show && ( + {tablePreferences.deltaPPM.show && ( )} - {preferences.deltaHz.show && ( + {tablePreferences.deltaHz.show && ( )} - {preferences.relative.show && ( + {tablePreferences.relative.show && ( )} - {preferences.absolute.show && ( + {tablePreferences.absolute.show && ( )} - {preferences.showMultiplicity && ( + {tablePreferences.showMultiplicity && ( )} - {preferences.coupling.show && ( + {tablePreferences.coupling.show && ( )} - {preferences.showAssignment && ( + {tablePreferences.showAssignment && ( ); diff --git a/src/component/reducer/preferences/panelsPreferencesDefaultValues.ts b/src/component/reducer/preferences/panelsPreferencesDefaultValues.ts index 4bb5f7d31..d42075251 100644 --- a/src/component/reducer/preferences/panelsPreferencesDefaultValues.ts +++ b/src/component/reducer/preferences/panelsPreferencesDefaultValues.ts @@ -1,10 +1,12 @@ import type { BaseNucleus1DPreferences, BaseNucleus2DPreferences, + BaseRangesTablePreferences, MatrixGenerationOptions, MultipleSpectraAnalysisPreferences, PanelsPreferences, PeaksNucleusPreferences, + RangesNucleusPreferences, SpectraPreferences, Zones1DNucleusPreferences, Zones2DNucleusPreferences, @@ -124,26 +126,33 @@ const getZoneDefaultValues = (nucleus?: string): PanelsPreferences['zones'] => { } }; +const createBaseRangesTablePreferences = (): BaseRangesTablePreferences => ({ + showSerialNumber: true, + from: { show: false, format: '0.00' }, + to: { show: false, format: '0.00' }, + absolute: { show: false, format: '0.00' }, + relative: { show: true, format: '0.00' }, + deltaPPM: { show: true, format: '0.00' }, + deltaHz: { show: false, format: '0.00' }, + coupling: { show: true, format: '0.00' }, + showKind: true, + showMultiplicity: true, + showAssignment: true, + showAssignmentLabel: true, +}); + const getRangeDefaultValues = ( nucleus?: string, ): PanelsPreferences['ranges'] => { - const preferences = { - showSerialNumber: true, - from: { show: false, format: '0.00' }, - to: { show: false, format: '0.00' }, - absolute: { show: false, format: '0.00' }, - relative: { show: true, format: '0.00' }, - deltaPPM: { show: true, format: '0.00' }, - deltaHz: { show: false, format: '0.00' }, - coupling: { show: true, format: '0.00' }, + const preferences: RangesNucleusPreferences = { + floatingTablePreferences: createBaseRangesTablePreferences(), + tablePreferences: { + ...createBaseRangesTablePreferences(), + showDeleteAction: true, + showZoomAction: true, + showEditAction: true, + }, jGraphTolerance: isProton(nucleus || '') ? 0.2 : nucleus === '13C' ? 2 : 0, //J Graph tolerance for: 1H: 0.2Hz 13C: 2Hz - showKind: true, - showMultiplicity: true, - showAssignment: true, - showDeleteAction: true, - showZoomAction: true, - showEditAction: true, - showAssignmentLabel: true, isSumConstant: true, }; @@ -154,17 +163,19 @@ const getPeaksDefaultValues = ( nucleus?: string, ): PanelsPreferences['peaks'] => { const preferences: PeaksNucleusPreferences = { - showSerialNumber: true, - deltaPPM: { show: true, format: '0.00' }, - deltaHz: { show: false, format: '0.00' }, - peakWidth: { show: false, format: '0.00' }, - intensity: { show: true, format: '0.00' }, - fwhm: { show: true, format: '0.00000' }, - mu: { show: false, format: '0.00000' }, - gamma: { show: false, format: '0.000' }, - showDeleteAction: true, - showEditPeakShapeAction: true, - showKind: true, + tablePreferences: { + showSerialNumber: true, + deltaPPM: { show: true, format: '0.00' }, + deltaHz: { show: false, format: '0.00' }, + peakWidth: { show: false, format: '0.00' }, + intensity: { show: true, format: '0.00' }, + fwhm: { show: true, format: '0.00000' }, + mu: { show: false, format: '0.00000' }, + gamma: { show: false, format: '0.000' }, + showKind: true, + showDeleteAction: true, + showEditPeakShapeAction: true, + }, defaultPeakShape: DEFAULT_PEAK_SHAPE, }; return getPreferences(preferences, nucleus); diff --git a/src/demo/views/SnapshotView.tsx b/src/demo/views/SnapshotView.tsx index 7bd5188fb..3a2619583 100644 --- a/src/demo/views/SnapshotView.tsx +++ b/src/demo/views/SnapshotView.tsx @@ -329,84 +329,159 @@ const customWorkspaces: CustomWorkspaces = { ranges: { nuclei: { '1H': { - isSumConstant: false, - showAssignment: false, - showAssignmentLabel: false, - showSerialNumber: false, - showDeleteAction: false, - showEditAction: false, - showMultiplicity: false, - showZoomAction: false, - from: { - show: false, - format: '0.00', - }, - to: { - show: false, - format: '0.00', - }, - absolute: { - show: false, - format: '0.00', - }, - relative: { - show: true, - format: '0.00', - }, - deltaPPM: { - show: true, - format: '0.00', - }, - deltaHz: { - show: false, - format: '0.00', + tablePreferences: { + showAssignment: false, + showAssignmentLabel: false, + showSerialNumber: false, + showMultiplicity: false, + from: { + show: false, + format: '0.00', + }, + to: { + show: false, + format: '0.00', + }, + absolute: { + show: false, + format: '0.00', + }, + relative: { + show: true, + format: '0.00', + }, + deltaPPM: { + show: true, + format: '0.00', + }, + deltaHz: { + show: false, + format: '0.00', + }, + coupling: { + show: false, + format: '0.00', + }, + showKind: false, + showDeleteAction: false, + showEditAction: false, + showZoomAction: false, }, - coupling: { - show: false, - format: '0.00', + floatingTablePreferences: { + showAssignment: false, + showAssignmentLabel: false, + showSerialNumber: false, + showMultiplicity: false, + from: { + show: false, + format: '0.00', + }, + to: { + show: false, + format: '0.00', + }, + absolute: { + show: false, + format: '0.00', + }, + relative: { + show: true, + format: '0.00', + }, + deltaPPM: { + show: true, + format: '0.00', + }, + deltaHz: { + show: false, + format: '0.00', + }, + coupling: { + show: false, + format: '0.00', + }, + showKind: false, }, + isSumConstant: false, jGraphTolerance: 0.2, - showKind: false, }, '13C': { - isSumConstant: false, - showAssignment: false, - showAssignmentLabel: false, - showSerialNumber: false, - showDeleteAction: false, - showEditAction: false, - showMultiplicity: false, - showZoomAction: false, - from: { - show: false, - format: '0.00', - }, - to: { - show: false, - format: '0.00', - }, - absolute: { - show: false, - format: '0.00', - }, - relative: { - show: true, - format: '0.00', - }, - deltaPPM: { - show: true, - format: '0.00', - }, - deltaHz: { - show: false, - format: '0.00', + tablePreferences: { + showAssignment: false, + showAssignmentLabel: false, + showSerialNumber: false, + showMultiplicity: false, + from: { + show: false, + format: '0.00', + }, + to: { + show: false, + format: '0.00', + }, + absolute: { + show: false, + format: '0.00', + }, + relative: { + show: true, + format: '0.00', + }, + deltaPPM: { + show: true, + format: '0.00', + }, + deltaHz: { + show: false, + format: '0.00', + }, + coupling: { + show: true, + format: '0.00', + }, + showKind: true, + + showDeleteAction: false, + showEditAction: false, + showZoomAction: false, }, - coupling: { - show: true, - format: '0.00', + floatingTablePreferences: { + showAssignment: false, + showAssignmentLabel: false, + showSerialNumber: false, + showMultiplicity: false, + from: { + show: false, + format: '0.00', + }, + to: { + show: false, + format: '0.00', + }, + absolute: { + show: false, + format: '0.00', + }, + relative: { + show: true, + format: '0.00', + }, + deltaPPM: { + show: true, + format: '0.00', + }, + deltaHz: { + show: false, + format: '0.00', + }, + coupling: { + show: true, + format: '0.00', + }, + showKind: true, }, + isSumConstant: false, jGraphTolerance: 2, - showKind: true, }, }, }, From 428b2f138ed98d77fbc9f13da18dec848b0a0309 Mon Sep 17 00:00:00 2001 From: hamed musallam Date: Fri, 19 Jun 2026 12:49:14 +0200 Subject: [PATCH 3/8] feat: ranges table preferences --- .../FloatingRangeTablePreferencesModal.tsx | 167 ++++++++++++++++++ .../1d/{ => ranges}/FloatingRanges.tsx | 128 ++++++++------ src/component/main/NMRiumViewer.tsx | 2 +- 3 files changed, 240 insertions(+), 57 deletions(-) create mode 100644 src/component/1d/ranges/FloatingRangeTablePreferencesModal.tsx rename src/component/1d/{ => ranges}/FloatingRanges.tsx (73%) diff --git a/src/component/1d/ranges/FloatingRangeTablePreferencesModal.tsx b/src/component/1d/ranges/FloatingRangeTablePreferencesModal.tsx new file mode 100644 index 000000000..71f9b4559 --- /dev/null +++ b/src/component/1d/ranges/FloatingRangeTablePreferencesModal.tsx @@ -0,0 +1,167 @@ +import { DialogBody, DialogFooter } from '@blueprintjs/core'; +import { useMemo } from 'react'; +import { useForm } from 'react-hook-form'; +import { Button } from 'react-science/ui'; + +import { usePreferences } from '../../context/PreferencesContext.js'; +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 links :', + 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) => ( + + ))} + + +
+ +
+
+ + + ); +} + diff --git a/src/component/1d/FloatingRanges.tsx b/src/component/1d/ranges/FloatingRanges.tsx similarity index 73% rename from src/component/1d/FloatingRanges.tsx rename to src/component/1d/ranges/FloatingRanges.tsx index f14ac92d9..5f6f7a934 100644 --- a/src/component/1d/FloatingRanges.tsx +++ b/src/component/1d/ranges/FloatingRanges.tsx @@ -4,25 +4,28 @@ 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 { FaCog, 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'; +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 { useDialogToggle } from '../../hooks/useDialogToggle.ts'; +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'; + +import { FloatingRangeTablePreferencesModal } from './FloatingRangeTablePreferencesModal.tsx'; const ReactRnd = styled(Rnd)` border: 1px solid transparent; @@ -171,6 +174,7 @@ function DraggableRanges(props: DraggablePublicationStringProps) { const [isMoveActive, setIsMoveActive] = useState(false); const { percentToPixel, pixelToPercent } = useSVGUnitConverter(); const isExportProcessStart = useCheckExportStatus(); + const { dialog, closeDialog, openDialog } = useDialogToggle({ settingModal: false }) useEffect(() => { setBounding({ ...externalBounding }); @@ -263,12 +267,20 @@ function DraggableRanges(props: DraggablePublicationStringProps) { const actionButtons: ActionsButtonsPopoverProps['buttons'] = [ { icon: , - intent: 'none', title: 'Move ranges table', style: { cursor: 'move' }, className: 'handle', }, + { + icon: , + intent: 'none', + title: 'Table preferences', + onClick: () => { + openDialog('settingModal') + }, + + }, { icon: , intent: 'danger', @@ -292,45 +304,49 @@ function DraggableRanges(props: DraggablePublicationStringProps) { } 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' }} - > - + + 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' }} > - - - + + + + + + ); } diff --git a/src/component/main/NMRiumViewer.tsx b/src/component/main/NMRiumViewer.tsx index 303ebfa8f..5795f34ad 100644 --- a/src/component/main/NMRiumViewer.tsx +++ b/src/component/main/NMRiumViewer.tsx @@ -2,9 +2,9 @@ import type { CSSProperties, RefObject } from 'react'; import { useDeferredValue, useEffect } from 'react'; import { FloatPublicationString } from '../1d/FloatPublicationString.js'; -import { FloatingRanges } from '../1d/FloatingRanges.js'; import { Viewer1D } from '../1d/Viewer1D.js'; import { SpectraInsets } from '../1d/inset/SpectraInsets.js'; +import { FloatingRanges } from '../1d/ranges/FloatingRanges.tsx'; import { FloatMolecules } from '../1d-2d/components/FloatMoleculeStructures/FloatMolecules.js'; import { SVGRootContainer } from '../1d-2d/components/SVGRootContainer.js'; import Viewer2D from '../2d/Viewer2D.js'; From c0427e74f4d19771009c45248b85be1f44ab8927 Mon Sep 17 00:00:00 2001 From: hamed musallam Date: Mon, 22 Jun 2026 11:31:40 +0200 Subject: [PATCH 4/8] refactor: svg table --- src/component/SVGTable.tsx | 68 ++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/src/component/SVGTable.tsx b/src/component/SVGTable.tsx index 2d7af78e0..397eb850c 100644 --- a/src/component/SVGTable.tsx +++ b/src/component/SVGTable.tsx @@ -1,5 +1,5 @@ -import type { SVGAttributes } from 'react'; -import { createContext, useContext, useMemo } from 'react'; +import type { ReactElement, SVGAttributes } from 'react'; +import { createContext, isValidElement, useContext, useMemo } from 'react'; type CellTextProps = Omit< SVGAttributes, @@ -15,7 +15,7 @@ interface StyleCell { cellBoxProps?: CellBoxProps; } interface BaseSVGTableColumn extends StyleCell { - header: string; + header: string | ReactElement; width: number; minWidth?: number; rowSpanGroupKey?: keyof T; @@ -27,7 +27,7 @@ interface AccessorKeyColumn extends BaseSVGTableColumn { } interface AccessorFuncColumn extends BaseSVGTableColumn { - accessorFun: (row: T) => number | string; + accessorFun: (row: T & { index: number }) => number | string; } export type SVGTableColumn = AccessorKeyColumn | AccessorFuncColumn; @@ -69,14 +69,14 @@ function mapColumns(columns: Array>) { } interface FormatKeyOptions { - rowIndex: number; + index: number; columnKey: string; groupKey: string; } function formatKey(options: FormatKeyOptions) { - const { rowIndex, columnKey, groupKey } = options; - return `GroupKey[${groupKey || null}]-ColumnKey[${columnKey}]-RowIndex[${rowIndex}]`; + const { index, columnKey, groupKey } = options; + return `GroupKey[${groupKey || null}]-ColumnKey[${columnKey}]-RowIndex[${index}]`; } function mapRowsSpan(data: T[], columns: Array>) { @@ -89,14 +89,14 @@ function mapRowsSpan(data: T[], columns: Array>) { let lastRowIndex = 0; let lastGroupKey: string | null = null; - for (let rowIndex = 0; rowIndex < data.length; rowIndex++) { - const row = data[rowIndex]; + for (let index = 0; index < data.length; index++) { + const row = data[index]; const groupKey = String(row?.[col.rowSpanGroupKey] || ''); const key = col._columnOptions.key; const value = isAccessorKeyColumn(col) ? (row as any)[col.accessorKey] - : col.accessorFun(row); + : col.accessorFun({ ...row, index }); if ( value === lastValue && @@ -107,16 +107,16 @@ function mapRowsSpan(data: T[], columns: Array>) { const prevKey = formatKey({ groupKey, columnKey: key, - rowIndex: lastRowIndex, + index: lastRowIndex, }); rowSpanMap.set(prevKey, (rowSpanMap.get(prevKey) || 1) + 1); - skipColumns.add(formatKey({ groupKey, columnKey: key, rowIndex })); + skipColumns.add(formatKey({ groupKey, columnKey: key, index })); } else { // Start a new rowspan lastValue = value; - lastRowIndex = rowIndex; + lastRowIndex = index; lastGroupKey = groupKey; - rowSpanMap.set(formatKey({ groupKey, columnKey: key, rowIndex }), 1); + rowSpanMap.set(formatKey({ groupKey, columnKey: key, index }), 1); } } } @@ -170,11 +170,11 @@ export function SVGTable(props: SVGTableProps) { ); })} - {data.map((row, rowIndex) => { + {data.map((row, index) => { return ( {columns.map((col) => { const { @@ -187,7 +187,7 @@ export function SVGTable(props: SVGTableProps) { const cellKey = formatKey({ groupKey: String(groupKey), columnKey, - rowIndex, + index, }); if (skipColumns.has(cellKey)) { return null; // Skip merged row @@ -199,7 +199,7 @@ export function SVGTable(props: SVGTableProps) { extends StyleCell { } interface ColumnProps extends BaseColumnProps { - value: string | number; + value: string | number | ReactElement; } interface ValueColumnProps extends BaseColumnProps { - row: T; + row: T & { index: number }; } function ValueColumn(props: ValueColumnProps) { @@ -250,10 +250,8 @@ function Column(props: ColumnProps) { const { rowHeight } = tableOptions; return ( - + (props: ColumnProps) { strokeWidth="1" {...cellBoxProps} /> - - {value} - + {isValidElement(value) ? ( + value + ) : ( + + {value} + + )} ); } From 9626981f2c222e5033ebbc2efcecf3154b60d906 Mon Sep 17 00:00:00 2001 From: hamed musallam Date: Mon, 22 Jun 2026 11:32:24 +0200 Subject: [PATCH 5/8] refactor: improve floating ranges table --- src/component/1d/peaks/usePeakShapesPath.ts | 12 +- .../FloatingRangeTablePreferencesModal.tsx | 48 +-- src/component/1d/ranges/FloatingRanges.tsx | 347 ++++++++++++------ 3 files changed, 260 insertions(+), 147 deletions(-) diff --git a/src/component/1d/peaks/usePeakShapesPath.ts b/src/component/1d/peaks/usePeakShapesPath.ts index 18e9f71f0..544fd5dcd 100644 --- a/src/component/1d/peaks/usePeakShapesPath.ts +++ b/src/component/1d/peaks/usePeakShapesPath.ts @@ -9,13 +9,13 @@ import { PathBuilder } from '../../utility/PathBuilder.js'; type PeaksShapesOptions = | { - target: 'peakShape'; - peak: Peak1D; - } + target: 'peakShape'; + peak: Peak1D; + } | { - target: 'peaksSum'; - peaks: Peak1D[]; - }; + target: 'peaksSum'; + peaks: Peak1D[]; + }; export function usePeakShapesPath(spectrum: Spectrum1D) { const { scaleX, scaleY } = useScaleChecked(); diff --git a/src/component/1d/ranges/FloatingRangeTablePreferencesModal.tsx b/src/component/1d/ranges/FloatingRangeTablePreferencesModal.tsx index 71f9b4559..68d8ac875 100644 --- a/src/component/1d/ranges/FloatingRangeTablePreferencesModal.tsx +++ b/src/component/1d/ranges/FloatingRangeTablePreferencesModal.tsx @@ -11,9 +11,6 @@ import type { NucleusPreferenceField } from '../../panels/extra/preferences/Nucl import { NucleusPreferences } from '../../panels/extra/preferences/NucleusPreferences.tsx'; import { getUniqueNuclei } from '../../utility/getUniqueNuclei.js'; - - - const formatFields: NucleusPreferenceField[] = [ { id: 1, @@ -81,12 +78,6 @@ const formatFields: NucleusPreferenceField[] = [ checkFieldName: 'floatingTablePreferences.showMultiplicity', hideFormatField: true, }, - { - id: 12, - label: 'Assignment links :', - checkFieldName: 'floatingTablePreferences.showAssignment', - hideFormatField: true, - }, ]; interface InnerFloatingRangeTablePreferencesModalProps { @@ -97,32 +88,34 @@ interface FloatingRangeTablePreferencesModalProps extends InnerFloatingRangeTabl isOpen: boolean; } -export function FloatingRangeTablePreferencesModal(props: FloatingRangeTablePreferencesModalProps) { +export function FloatingRangeTablePreferencesModal( + props: FloatingRangeTablePreferencesModalProps, +) { const { onCloseDialog, isOpen } = props; if (!isOpen) return; - - - return - - + return ( + + + + ); } -function InnerFloatingRangeTablePreferencesModal(props: InnerFloatingRangeTablePreferencesModalProps) { +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, }); @@ -134,10 +127,8 @@ function InnerFloatingRangeTablePreferencesModal(props: InnerFloatingRangeTableP }); onCloseDialog(); })(); - } - return ( <> @@ -152,16 +143,11 @@ function InnerFloatingRangeTablePreferencesModal(props: InnerFloatingRangeTableP
-
- ); } - diff --git a/src/component/1d/ranges/FloatingRanges.tsx b/src/component/1d/ranges/FloatingRanges.tsx index 5f6f7a934..d88e1406f 100644 --- a/src/component/1d/ranges/FloatingRanges.tsx +++ b/src/component/1d/ranges/FloatingRanges.tsx @@ -1,10 +1,13 @@ import styled from '@emotion/styled'; -import type { Ranges } from '@zakodium/nmr-types'; -import type { BoundingBox } from '@zakodium/nmrium-core'; +import type { Info1D, Range, Ranges, Signal1D } from '@zakodium/nmr-types'; +import type { + BaseRangesTablePreferences, + BoundingBox, +} from '@zakodium/nmrium-core'; import { checkMultiplicity } from 'nmr-processing'; -import { memo, useEffect, useState } from 'react'; +import { memo, useEffect, useMemo, useState } from 'react'; import { BsArrowsMove } from 'react-icons/bs'; -import { FaCog, FaTimes } from 'react-icons/fa'; +import { FaCog, FaLink, FaTimes } from 'react-icons/fa'; import { Rnd } from 'react-rnd'; import { isSpectrum1D } from '../../../data/data1d/Spectrum1D/index.js'; @@ -39,142 +42,265 @@ const ReactRnd = styled(Rnd)` } } `; +interface SpectrumRangesInfo { + ranges: Ranges['values']; + info: Info1D; +} interface RangesTableProps { - ranges: Ranges['values']; + data: SpectrumRangesInfo; } interface RangeItem { id: string; delta: string; + deltaHz: string; multiplicity: string; integration: string; coupling: string; + from: string; + to: string; + absolute: string; + kind: string; + assignment: string; + nbAssignment: string; +} + +function buildRangeBaseItem( + range: Range, + info: Info1D, + preferences: BaseRangesTablePreferences, +) { + const { id, from, to, integration, absolute } = range; + const formatHz = preferences.deltaHz.format; + return { + id, + from: formatNumber(from, preferences.from.format), + to: formatNumber(to, preferences.to.format), + absolute: formatNumber(absolute, preferences.absolute.format), + integration: isSignalRange(range) + ? formatNumber(integration, preferences.relative.format) + : `[ ${formatNumber(integration, preferences.relative.format)} ]`, + deltaText: `${formatNumber(from, preferences.from.format)} - ${formatNumber(to, preferences.to.format)}`, + deltaHzText: `${formatNumber(from * info.originFrequency, formatHz)} - ${formatNumber(to * info.originFrequency, formatHz)}`, + formatHz, + }; } -function useMapRanges(ranges: Ranges['values']) { - const output: RangeItem[] = []; +function buildSignalItem( + signal: Signal1D, + base: ReturnType, + info: Info1D, + preferences: BaseRangesTablePreferences, +): RangeItem { + const { + multiplicity, + delta, + kind = '', + assignment = '', + js = [], + diaIDs, + } = signal; + const isNotMultiplet = !checkMultiplicity(multiplicity, ['m']); + return { + id: base.id, + from: base.from, + to: base.to, + absolute: base.absolute, + integration: base.integration, + multiplicity, + kind, + assignment, + coupling: js + .map((j) => + !Number.isNaN(j.coupling) + ? formatNumber(j.coupling, preferences.coupling.format) + : '', + ) + .join(','), + delta: isNotMultiplet + ? base.deltaText + : formatNumber(delta, preferences.deltaPPM.format), + deltaHz: isNotMultiplet + ? base.deltaHzText + : info?.originFrequency + ? formatNumber(delta * info.originFrequency, base.formatHz) + : '', + nbAssignment: diaIDs?.length ? String(diaIDs.length) : '', + }; +} + +function useMapRanges(data: SpectrumRangesInfo): RangeItem[] { + const { ranges, info } = data; const activeTab = useActiveNucleusTab(); - const { tablePreferences } = usePanelPreferences('ranges', activeTab); - for (const range of ranges) { - const { id, from, to, integration, signals = [] } = range; - const relativeFlag = isSignalRange(range); - const formattedValue = formatNumber( - integration, - tablePreferences.relative.format, - ); - const integrationValue = relativeFlag - ? formattedValue - : `[ ${formattedValue} ]`; - - const rangeText = `${formatNumber(from, tablePreferences.from.format)} - ${formatNumber( - to, - tablePreferences.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, tablePreferences.coupling.format) - : '', - ) - .join(','); - const signalDelta = !checkMultiplicity(multiplicity, ['m']) - ? rangeText - : formatNumber(delta, tablePreferences.deltaPPM.format); - output.push({ - id, - delta: signalDelta, - multiplicity, - integration: integrationValue, - coupling, - }); - } - } else { - output.push({ - id, - delta: rangeText, - multiplicity: 'm', - integration: integrationValue, - coupling: '', - }); + const { floatingTablePreferences: preferences } = usePanelPreferences( + 'ranges', + activeTab, + ); + + return ranges.flatMap((range) => { + const base = buildRangeBaseItem(range, info, preferences); + const { signals = [] } = range; + + if (signals.length === 0) { + return [ + { + ...base, + delta: base.deltaText, + deltaHz: '', + multiplicity: 'm', + coupling: '', + kind: '', + assignment: '', + nbAssignment: '', + }, + ]; } - } - return output; + + return signals.map((signal) => + buildSignalItem(signal, base, info, preferences), + ); + }); +} + +function isColumnVisible( + pref: BaseRangesTablePreferences[keyof BaseRangesTablePreferences], +): boolean { + if (typeof pref === 'boolean') return pref; + return pref?.show; } + function InnerSVGRangesTable(props: RangesTableProps) { - const { ranges } = props; + const { data } = props; const { view: { spectra: { activeTab }, }, } = useChartData(); - const data = useMapRanges(ranges); - - if (!data) return null; + const rangesData = useMapRanges(data); + const { floatingTablePreferences } = usePanelPreferences('ranges', activeTab); 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' }, - }, - ]; + const columns = useMemo((): Array> => { + const allColumns: Array< + { prefKey: keyof BaseRangesTablePreferences } & SVGTableColumn + > = [ + { + prefKey: 'showSerialNumber', + accessorFun: (row) => row.index + 1, + header: '#', + width: 40, + }, + { + prefKey: 'showAssignmentLabel', + accessorKey: 'assignment', + header: 'Assignment', + width: 120, + }, + { + prefKey: 'deltaPPM', + accessorKey: 'delta', + header: 'δ (ppm)', + width: 100, + rowSpanGroupKey: 'id', + }, + { + prefKey: 'deltaHz', + accessorKey: 'deltaHz', + header: 'δ (Hz)', + width: 110, + rowSpanGroupKey: 'id', + }, + { + prefKey: 'from', + accessorKey: 'from', + header: `From`, + width: 60, + }, + { + prefKey: 'to', + accessorKey: 'to', + header: `To`, + width: 60, + }, + { + prefKey: 'relative', + accessorKey: 'integration', + header: `Rel. ${element}`, + width: 60, + rowSpanGroupKey: 'id', + }, + { + prefKey: 'absolute', + accessorKey: 'absolute', + header: 'Absolute', + width: 80, + rowSpanGroupKey: 'id', + }, + { + prefKey: 'showMultiplicity', + accessorKey: 'multiplicity', + header: 'Mult.', + width: 60, + rowSpanGroupKey: 'id', + }, + { + prefKey: 'coupling', + accessorKey: 'coupling', + header: 'J (Hz)', + width: 120, + }, + + { + prefKey: 'showAssignment', + accessorKey: 'nbAssignment', + header: , + width: 40, + }, + { + prefKey: 'showKind', + accessorKey: 'kind', + header: 'Kind', + width: 120, + }, + ]; + + return allColumns + .filter(({ prefKey }) => + isColumnVisible(floatingTablePreferences[prefKey]), + ) + .map(({ prefKey: _, ...col }) => ({ + ...col, + headerTextProps: { fontWeight: 'bold' }, + cellBoxProps: { stroke: '#dedede', fill: 'white', fillOpacity: 0.8 }, + headerBoxProps: { stroke: '#dedede', fill: '#E5E8EB' }, + })); + }, [floatingTablePreferences, element]); + if (!data) return null; - return data={data} columns={columns} />; + return data={rangesData} columns={columns} />; } const SVGRangesTable = memo(InnerSVGRangesTable); interface DraggablePublicationStringProps { - ranges: Ranges['values'] | undefined; + data: SpectrumRangesInfo | undefined; bonding: BoundingBox; spectrumKey: string; } function DraggableRanges(props: DraggablePublicationStringProps) { - const { ranges = [], bonding: externalBounding, spectrumKey } = props; + const { data, 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(); - const { dialog, closeDialog, openDialog } = useDialogToggle({ settingModal: false }) + const { dialog, closeDialog, openDialog } = useDialogToggle({ + settingModal: false, + }); useEffect(() => { setBounding({ ...externalBounding }); @@ -277,9 +403,8 @@ function DraggableRanges(props: DraggablePublicationStringProps) { intent: 'none', title: 'Table preferences', onClick: () => { - openDialog('settingModal') + openDialog('settingModal'); }, - }, { icon: , @@ -288,7 +413,7 @@ function DraggableRanges(props: DraggablePublicationStringProps) { onClick: handleRemove, }, ]; - if (!viewerRef || ranges.length === 0) return null; + if (!viewerRef || !data || data?.ranges?.length === 0) return null; const { x: xInPercent, y: yInPercent } = bounding; @@ -298,14 +423,17 @@ function DraggableRanges(props: DraggablePublicationStringProps) { if (isExportProcessStart) { return ( - + ); } return ( <> - + - + - ); } function useSpectraRanges() { const spectra = useSpectraByActiveNucleus(); - const output: Record = {}; + const output: Record = {}; for (const spectrum of spectra) { if (!isSpectrum1D(spectrum)) { continue; } - const { id: spectrumKey, ranges } = spectrum; + const { id: spectrumKey, ranges, info } = spectrum; if (!Array.isArray(ranges?.values) || ranges.values.length === 0) { continue; } - output[spectrumKey] = ranges.values; + output[spectrumKey] = { ranges: ranges.values, info }; } return output; @@ -387,7 +514,7 @@ export function FloatingRanges() { key={spectrumKey} spectrumKey={spectrumKey} bonding={rangesBounding} - ranges={spectraRanges[spectrumKey]} + data={spectraRanges[spectrumKey]} /> ); }); From aaf37ad6fb8fdf09a2d8b9901a6244036db411fc Mon Sep 17 00:00:00 2001 From: hamed musallam Date: Tue, 23 Jun 2026 09:16:19 +0200 Subject: [PATCH 6/8] fix: show assignemnt --- .../FloatingRangeTablePreferencesModal.tsx | 6 + src/component/1d/ranges/FloatingRanges.tsx | 156 +++++++++--------- 2 files changed, 84 insertions(+), 78 deletions(-) diff --git a/src/component/1d/ranges/FloatingRangeTablePreferencesModal.tsx b/src/component/1d/ranges/FloatingRangeTablePreferencesModal.tsx index 68d8ac875..95e8ba58d 100644 --- a/src/component/1d/ranges/FloatingRangeTablePreferencesModal.tsx +++ b/src/component/1d/ranges/FloatingRangeTablePreferencesModal.tsx @@ -78,6 +78,12 @@ const formatFields: NucleusPreferenceField[] = [ checkFieldName: 'floatingTablePreferences.showMultiplicity', hideFormatField: true, }, + { + id: 12, + label: 'Assignment :', + checkFieldName: 'floatingTablePreferences.showAssignment', + hideFormatField: true, + }, ]; interface InnerFloatingRangeTablePreferencesModalProps { diff --git a/src/component/1d/ranges/FloatingRanges.tsx b/src/component/1d/ranges/FloatingRanges.tsx index d88e1406f..9bba9be7b 100644 --- a/src/component/1d/ranges/FloatingRanges.tsx +++ b/src/component/1d/ranges/FloatingRanges.tsx @@ -186,85 +186,85 @@ function InnerSVGRangesTable(props: RangesTableProps) { const allColumns: Array< { prefKey: keyof BaseRangesTablePreferences } & SVGTableColumn > = [ - { - prefKey: 'showSerialNumber', - accessorFun: (row) => row.index + 1, - header: '#', - width: 40, - }, - { - prefKey: 'showAssignmentLabel', - accessorKey: 'assignment', - header: 'Assignment', - width: 120, - }, - { - prefKey: 'deltaPPM', - accessorKey: 'delta', - header: 'δ (ppm)', - width: 100, - rowSpanGroupKey: 'id', - }, - { - prefKey: 'deltaHz', - accessorKey: 'deltaHz', - header: 'δ (Hz)', - width: 110, - rowSpanGroupKey: 'id', - }, - { - prefKey: 'from', - accessorKey: 'from', - header: `From`, - width: 60, - }, - { - prefKey: 'to', - accessorKey: 'to', - header: `To`, - width: 60, - }, - { - prefKey: 'relative', - accessorKey: 'integration', - header: `Rel. ${element}`, - width: 60, - rowSpanGroupKey: 'id', - }, - { - prefKey: 'absolute', - accessorKey: 'absolute', - header: 'Absolute', - width: 80, - rowSpanGroupKey: 'id', - }, - { - prefKey: 'showMultiplicity', - accessorKey: 'multiplicity', - header: 'Mult.', - width: 60, - rowSpanGroupKey: 'id', - }, - { - prefKey: 'coupling', - accessorKey: 'coupling', - header: 'J (Hz)', - width: 120, - }, + { + prefKey: 'showSerialNumber', + accessorFun: (row) => row.index + 1, + header: '#', + width: 40, + }, + { + prefKey: 'showAssignmentLabel', + accessorKey: 'assignment', + header: 'Assignment', + width: 120, + }, + { + prefKey: 'deltaPPM', + accessorKey: 'delta', + header: 'δ (ppm)', + width: 100, + rowSpanGroupKey: 'id', + }, + { + prefKey: 'deltaHz', + accessorKey: 'deltaHz', + header: 'δ (Hz)', + width: 110, + rowSpanGroupKey: 'id', + }, + { + prefKey: 'from', + accessorKey: 'from', + header: `From`, + width: 60, + }, + { + prefKey: 'to', + accessorKey: 'to', + header: `To`, + width: 60, + }, + { + prefKey: 'relative', + accessorKey: 'integration', + header: `Rel. ${element}`, + width: 60, + rowSpanGroupKey: 'id', + }, + { + prefKey: 'absolute', + accessorKey: 'absolute', + header: 'Absolute', + width: 80, + rowSpanGroupKey: 'id', + }, + { + prefKey: 'showMultiplicity', + accessorKey: 'multiplicity', + header: 'Mult.', + width: 60, + rowSpanGroupKey: 'id', + }, + { + prefKey: 'coupling', + accessorKey: 'coupling', + header: 'J (Hz)', + width: 120, + }, - { - prefKey: 'showAssignment', - accessorKey: 'nbAssignment', - header: , - width: 40, - }, - { - prefKey: 'showKind', - accessorKey: 'kind', - header: 'Kind', - width: 120, - }, - ]; + { + prefKey: 'showAssignment', + accessorKey: 'nbAssignment', + header: , + width: 40, + }, + { + prefKey: 'showKind', + accessorKey: 'kind', + header: 'Kind', + width: 120, + }, + ]; return allColumns .filter(({ prefKey }) => From 9b2e48bca76cb5c5711efd2aa813c6e968c131f2 Mon Sep 17 00:00:00 2001 From: hamed musallam Date: Tue, 23 Jun 2026 10:23:21 +0200 Subject: [PATCH 7/8] feat: filter floating ranges table by signal kinds --- .../FloatingRangeTablePreferencesModal.tsx | 85 ++++++++++++++++++- src/component/1d/ranges/FloatingRanges.tsx | 14 ++- .../panelsPreferencesDefaultValues.ts | 32 +++---- src/data/constants/signalsKinds.ts | 2 +- src/demo/views/SnapshotView.tsx | 2 + 5 files changed, 113 insertions(+), 22 deletions(-) diff --git a/src/component/1d/ranges/FloatingRangeTablePreferencesModal.tsx b/src/component/1d/ranges/FloatingRangeTablePreferencesModal.tsx index 95e8ba58d..6bcf29de0 100644 --- a/src/component/1d/ranges/FloatingRangeTablePreferencesModal.tsx +++ b/src/component/1d/ranges/FloatingRangeTablePreferencesModal.tsx @@ -1,9 +1,16 @@ -import { DialogBody, DialogFooter } from '@blueprintjs/core'; +import { DialogBody, DialogFooter, MenuItem } from '@blueprintjs/core'; +import { MultiSelect } from '@blueprintjs/select'; +import type { SignalKind } from '@zakodium/nmr-types'; import { useMemo } from 'react'; -import { useForm } from 'react-hook-form'; +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'; @@ -121,7 +128,6 @@ function InnerFloatingRangeTablePreferencesModal( const nucleus = useNucleus(); const nuclei = useMemo(() => getUniqueNuclei(nucleus), [nucleus]); const preferencesByNuclei = usePanelPreferencesByNuclei('ranges', nuclei); - const { handleSubmit, control } = useForm({ defaultValues: preferencesByNuclei, }); @@ -144,6 +150,9 @@ function InnerFloatingRangeTablePreferencesModal( control={control} nucleus={n} fields={formatFields} + renderBottom={() => ( + + )} /> ))} @@ -157,3 +166,73 @@ function InnerFloatingRangeTablePreferencesModal( ); } + +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 ( +
##Assignment From {isSortedDesc('from').content} To {isSortedDesc('to').content} δ (ppm) {isSortedDesc('from').content} δ (Hz) δ (Hz) Rel. {element} {isSortedDesc('integration').content} AbsoluteMult.J (Hz)AbsoluteMult.J (Hz) KindKind{''}
{rowData.tableMetaInfo.rowIndex + 1} {rowData.tableMetaInfo.signal?.multiplicity ?? 'm'}