diff --git a/package-lock.json b/package-lock.json index 2fdcb0f9a..90d182933 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,7 @@ "ml-stat": "^1.3.3", "multiplet-analysis": "^2.1.2", "nmr-correlation": "^2.3.3", - "nmr-load-save": "^0.23.11", + "nmr-load-save": "^0.24.0", "nmr-processing": "^11.7.0", "nmredata": "^0.9.9", "numeral": "^2.0.6", @@ -9827,9 +9827,9 @@ } }, "node_modules/nmr-load-save": { - "version": "0.23.11", - "resolved": "https://registry.npmjs.org/nmr-load-save/-/nmr-load-save-0.23.11.tgz", - "integrity": "sha512-tvZDBfiZYy7xBUdx7MVVoQ3M3DzhsEKZCgkjkVk0tGVNcy1q9C1WYT48nSJXhgTMNslfH8GH9f2ojSTFnHoUDQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/nmr-load-save/-/nmr-load-save-0.24.0.tgz", + "integrity": "sha512-08Kouo4DjkJj6CPr4fgXO82oLZzexTqRqZOCtP2TDor+N7wae7yo8MtwotrPLY4VQDHoiRIy6A96KEuWviimZQ==", "dependencies": { "@lukeed/uuid": "^2.0.1", "@types/lodash.merge": "^4.6.7", @@ -14122,4 +14122,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index e20211f37..7d6cbd9a8 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "ml-stat": "^1.3.3", "multiplet-analysis": "^2.1.2", "nmr-correlation": "^2.3.3", - "nmr-load-save": "^0.23.11", + "nmr-load-save": "^0.24.0", "nmr-processing": "^11.7.0", "nmredata": "^0.9.9", "numeral": "^2.0.6", @@ -148,4 +148,4 @@ "vite": "^5.0.10", "vitest": "^1.0.4" } -} +} \ No newline at end of file diff --git a/src/component/1d/integral/Integration.tsx b/src/component/1d/integral/Integration.tsx index e4ee53496..c7f8663aa 100644 --- a/src/component/1d/integral/Integration.tsx +++ b/src/component/1d/integral/Integration.tsx @@ -15,6 +15,7 @@ export function Integration({ integral, nucleus, max }: IntegralProps) { const { x, y } = integral; const { scaleRatio } = useActiveSpectrumIntegralsViewState(); const path = useIntegralPath({ x, y, max, scaleRatio }); + const { showIntegralsValues } = useActiveSpectrumIntegralsViewState(); const integralPreferences = usePanelPreferences('integrals', nucleus); return ( @@ -27,10 +28,12 @@ export function Integration({ integral, nucleus, max }: IntegralProps) { d={path} /> - + {showIntegralsValues && ( + + )} ); } diff --git a/src/component/1d/ranges/Range.tsx b/src/component/1d/ranges/Range.tsx index 40e44b59c..381db940a 100644 --- a/src/component/1d/ranges/Range.tsx +++ b/src/component/1d/ranges/Range.tsx @@ -12,6 +12,7 @@ import { filterForIDsWithAssignment } from '../../assignment/utilities/filterFor import { useDispatch } from '../../context/DispatchContext'; import { ResizerWithScale } from '../../elements/ResizerWithScale'; import { HighlightEventSource, useHighlight } from '../../highlight'; +import { useActiveSpectrumRangesViewState } from '../../hooks/useActiveSpectrumRangesViewState'; import { useResizerStatus } from '../../hooks/useResizerStatus'; import { options } from '../../toolbar/ToolTypes'; import { IntegralIndicator } from '../integral/IntegralIndicator'; @@ -60,6 +61,7 @@ function Range({ const scaleX = useScaleX(); const dispatch = useDispatch(); + const { showIntegralsValues } = useActiveSpectrumRangesViewState(); const isBlockedByEditing = selectedTool && selectedTool === options.editRange.id; @@ -145,12 +147,14 @@ function Range({ data-no-export="true" /> - + {showIntegralsValues && ( + + )} ); }} diff --git a/src/component/elements/formik/FormikColorInput.tsx b/src/component/elements/formik/FormikColorInput.tsx deleted file mode 100644 index 9b0aad32e..000000000 --- a/src/component/elements/formik/FormikColorInput.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { useFormikContext } from 'formik'; -import lodashGet from 'lodash/get'; -import { useCallback, memo } from 'react'; - -import ColorInput, { ColorInputProps } from '../ColorInput'; - -interface FormikColorPickerProps extends ColorInputProps { - name: string; -} - -function FormikColorPicker(props: FormikColorPickerProps) { - const { onColorChange = () => null, name, ...colorPickerProps } = props; - - const { values, setFieldValue } = useFormikContext(); - - const colorChangeHandler = useCallback( - (color) => { - onColorChange(color); - void setFieldValue(name, color.hex); - }, - [name, onColorChange, setFieldValue], - ); - - return ( - - ); -} - -export default memo(FormikColorPicker); diff --git a/src/component/elements/formik/FormikColorPickerDropdown.tsx b/src/component/elements/formik/FormikColorPickerDropdown.tsx new file mode 100644 index 000000000..4030e4818 --- /dev/null +++ b/src/component/elements/formik/FormikColorPickerDropdown.tsx @@ -0,0 +1,28 @@ +import { useFormikContext } from 'formik'; +import lodashGet from 'lodash/get'; +import { ColorPickerDropdown, ColorPickerProps } from 'react-science/ui'; + +interface FormikColorPickerDropdownProps extends ColorPickerProps { + name: string; +} + +function FormikColorPickerDropdown(props: FormikColorPickerDropdownProps) { + const { onChangeComplete = () => null, name, ...colorPickerProps } = props; + + const { values, setFieldValue } = useFormikContext(); + + function colorChangeHandler(color) { + onChangeComplete(color); + void setFieldValue(name, color.hex); + } + + return ( + + ); +} + +export default FormikColorPickerDropdown; diff --git a/src/component/hooks/useActiveSpectrumIntegralsViewState.ts b/src/component/hooks/useActiveSpectrumIntegralsViewState.ts index 9d9246827..2aa38a8ea 100644 --- a/src/component/hooks/useActiveSpectrumIntegralsViewState.ts +++ b/src/component/hooks/useActiveSpectrumIntegralsViewState.ts @@ -6,6 +6,7 @@ import { useActiveSpectrum } from './useActiveSpectrum'; export const defaultIntegralsViewState: IntegralsViewState = { scaleRatio: 1, + showIntegralsValues: true, }; export function useActiveSpectrumIntegralsViewState() { diff --git a/src/component/hooks/useActiveSpectrumRangesViewState.ts b/src/component/hooks/useActiveSpectrumRangesViewState.ts index 7c408cad5..8637e6a07 100644 --- a/src/component/hooks/useActiveSpectrumRangesViewState.ts +++ b/src/component/hooks/useActiveSpectrumRangesViewState.ts @@ -8,6 +8,7 @@ export const defaultRangesViewState: RangesViewState = { showPeaks: false, showMultiplicityTrees: false, showIntegrals: false, + showIntegralsValues: true, showJGraph: false, displayingMode: 'spread', integralsScaleRatio: 1, diff --git a/src/component/loader/DropZone.tsx b/src/component/loader/DropZone.tsx index 72bf126bd..c535bbb38 100644 --- a/src/component/loader/DropZone.tsx +++ b/src/component/loader/DropZone.tsx @@ -54,7 +54,8 @@ const containerStyle = css` function DropZone(props) { const { width, height } = useChartData(); const dispatch = useDispatch(); - const { dispatch: dispatchPreferences, current } = usePreferences(); + const { dispatch: dispatchPreferences, current: workspacePreferences } = + usePreferences(); const preferences = usePreferences(); const isToolEnabled = useCheckToolsVisibility(); const openImportMetaInformationModal = useMetaInformationImportationModal(); @@ -76,12 +77,13 @@ function DropZone(props) { parseMetaFileResult = await parseMetaFile(metaFile); } const { nmrLoaders: sourceSelector } = preferences.current; + const { onLoadProcessing, spectraColors } = workspacePreferences; const { nmriumState, containsNmrium } = await readDropFiles( fileCollection, { sourceSelector, logger: logger.child({ context: 'nmr-processing' }), - onLoadProcessing: current.onLoadProcessing, + onLoadProcessing, experimentalFeatures, }, ); @@ -101,6 +103,7 @@ function DropZone(props) { nmriumState, containsNmrium, parseMetaFileResult, + spectraColors, }, }); } diff --git a/src/component/modal/setting/GeneralSettings.tsx b/src/component/modal/setting/GeneralSettings.tsx index 7489f8997..8bfb34b3c 100644 --- a/src/component/modal/setting/GeneralSettings.tsx +++ b/src/component/modal/setting/GeneralSettings.tsx @@ -33,6 +33,7 @@ import GeneralTabContent from './settings-tabs/GeneralTabContent'; import ImportationFiltersTabContent from './settings-tabs/ImportationFiltersTabContent'; import InfoBlockTabContent from './settings-tabs/InfoBlockTabContent'; import OnLoadProcessingTabContent from './settings-tabs/OnLoadProcessingTabContent'; +import SpectraColorsTabContent from './settings-tabs/SpectraColorsTabContent'; import ToolsTabContent from './settings-tabs/ToolsTabContent'; import { validation } from './settingsValidation'; @@ -422,6 +423,11 @@ function GeneralSettingsModal({ height }: GeneralSettingsModalProps) { + +
+ +
+
diff --git a/src/component/modal/setting/settings-tabs/SpectraColorsTabContent.tsx b/src/component/modal/setting/settings-tabs/SpectraColorsTabContent.tsx new file mode 100644 index 000000000..5197c4900 --- /dev/null +++ b/src/component/modal/setting/settings-tabs/SpectraColorsTabContent.tsx @@ -0,0 +1,258 @@ +import { useFormikContext } from 'formik'; +import { SpectraColors, Workspace } from 'nmr-load-save'; +import { CSSProperties, useCallback, useMemo } from 'react'; +import { FaPlus, FaTimes } from 'react-icons/fa'; + +import { useChartData } from '../../../context/ChartContext'; +import Button from '../../../elements/Button'; +import { GroupPane } from '../../../elements/GroupPane'; +import ReactTable, { Column } from '../../../elements/ReactTable/ReactTable'; +import FormikColorPickerDropdown from '../../../elements/formik/FormikColorPickerDropdown'; +import FormikInput from '../../../elements/formik/FormikInput'; +import { convertPathArrayToString } from '../../../utility/convertPathArrayToString'; +import { getSpectraObjectPaths } from '../../../utility/getSpectraObjectPaths'; + +const styles: Record<'input' | 'column', CSSProperties> = { + input: { + width: '100%', + }, + column: { + padding: '2px', + }, +}; + +const colorInputStyle: CSSProperties = { + padding: 0, + minWidth: '80px', + width: '80px', + ...styles.column, +}; + +type SpectraColorsKeys = keyof SpectraColors; + +function getObjectKey(key: SpectraColorsKeys) { + return `spectraColors.${key}`; +} + +function SpectraColorsTabContent() { + const { values, setFieldValue } = useFormikContext(); + const { data } = useChartData(); + const { datalist, paths } = useMemo( + () => getSpectraObjectPaths(data), + [data], + ); + const { oneDimension = [], twoDimensions = [] } = values?.spectraColors || {}; + + const addHandler = useCallback( + (data: readonly any[], index: number, key: SpectraColorsKeys) => { + let columns: any[] = []; + const emptyField = + key === 'oneDimension' + ? { + value: null, + jpath: ['info', 'experiment'], + color: 'red', + } + : { + value: null, + jpath: ['info', 'experiment'], + positiveColor: 'red', + negativeColor: 'blue', + }; + if (data && Array.isArray(data)) { + columns = [...data.slice(0, index), emptyField, ...data.slice(index)]; + } else { + columns.push(emptyField); + } + void setFieldValue(getObjectKey(key), columns); + }, + [setFieldValue], + ); + + const deleteHandler = useCallback( + (data, index: number, key: SpectraColorsKeys) => { + const _fields = data.filter((_, columnIndex) => columnIndex !== index); + void setFieldValue(getObjectKey(key), _fields); + }, + [setFieldValue], + ); + + return ( +
+ + +
+ ); +} + +interface SpectraColorsProps { + data: any; + onAdd: (data: any, index: number, key: SpectraColorsKeys) => void; + onDelete: (data: any, index: number, key: SpectraColorsKeys) => void; + datalist: string[]; + paths: Record; + baseObjectPath: SpectraColorsKeys; + groupLabel: string; +} +function SpectraColorsFields(props: SpectraColorsProps) { + const { onAdd, onDelete, datalist, paths, data, baseObjectPath, groupLabel } = + props; + + const COLUMNS: Array> = useMemo(() => { + const objectPath = getObjectKey(baseObjectPath); + const baseColumns = [ + { + Header: '#', + style: { width: '25px', ...styles.column }, + accessor: (_, index) => index + 1, + }, + { + Header: 'Field', + style: { padding: 0, ...styles.column }, + Cell: ({ row }) => { + return ( + { + return paths?.[key.toString().trim()] || key; + }} + mapValue={(paths) => convertPathArrayToString(paths)} + datalist={datalist} + /> + ); + }, + }, + { + Header: 'Value', + style: { padding: 0, ...styles.column }, + Cell: ({ row }) => { + return ( + + ); + }, + }, + ]; + + const operationsField = { + Header: '', + style: { width: '70px', ...styles.column }, + id: 'operation-button', + Cell: ({ data, row }) => { + const record: any = row.original; + return ( +
+ onAdd(data, row.index + 1, baseObjectPath)} + > + + + {!record?.name && ( + onDelete(data, row.index, baseObjectPath)} + > + + + )} +
+ ); + }, + }; + + if (baseObjectPath === 'oneDimension') { + const colorField = { + Header: 'Color', + style: colorInputStyle, + Cell: ({ row }) => ( + + ), + }; + + return [...baseColumns, colorField, operationsField]; + } + + const colorFields = [ + { + Header: 'Positive color', + style: colorInputStyle, + Cell: ({ row }) => ( + + ), + }, + { + Header: 'Negative color', + style: colorInputStyle, + Cell: ({ row }) => ( + + ), + }, + ]; + + return [...baseColumns, ...colorFields, operationsField]; + }, [baseObjectPath, datalist, onAdd, onDelete, paths]); + + return ( + { + return ( + onAdd(data, 0, baseObjectPath)} + /> + ); + }} + > + + + ); +} + +function FieldsBlockHeader({ onAdd, text }) { + return ( +
+

{text}

+ + + Add custom color + +
+ ); +} + +export default SpectraColorsTabContent; diff --git a/src/component/modal/setting/settingsValidation.ts b/src/component/modal/setting/settingsValidation.ts index 4a59d05c7..3ccfe8d45 100644 --- a/src/component/modal/setting/settingsValidation.ts +++ b/src/component/modal/setting/settingsValidation.ts @@ -81,7 +81,21 @@ const infoBlockValidation = object({ fields: array().of( object({ label: string().required(), - jpath: array().of(string()).min(1), + jpath: array().of(string()).required().min(1), + }), + ), +}); +const spectraColorsSchemaValidation = object({ + oneDimension: array().of( + object({ + value: string().required(), + jpath: array().of(string()).required().min(1), + }), + ), + twoDimensions: array().of( + object({ + value: string().required(), + jpath: array().of(string()).required().min(1), }), ), }); @@ -95,5 +109,6 @@ export const validation: any = lazy((obj: Workspace) => databases: databasesValidation, infoBlock: infoBlockValidation, general: generalValidation, + spectraColors: spectraColorsSchemaValidation, }), ); diff --git a/src/component/panels/IntegralsPanel/IntegralPanel.tsx b/src/component/panels/IntegralsPanel/IntegralPanel.tsx index b78981870..044c1fa65 100644 --- a/src/component/panels/IntegralsPanel/IntegralPanel.tsx +++ b/src/component/panels/IntegralsPanel/IntegralPanel.tsx @@ -1,6 +1,6 @@ /** @jsxImportSource @emotion/react */ import { css } from '@emotion/react'; -import { SvgNmrSum } from 'cheminfo-font'; +import { SvgNmrIntegrate, SvgNmrSum } from 'cheminfo-font'; import lodashGet from 'lodash/get'; import { Spectrum1D } from 'nmr-load-save'; import { Info1D, Integrals } from 'nmr-processing'; @@ -22,6 +22,7 @@ import PreferencesHeader from '../header/PreferencesHeader'; import IntegralTable from './IntegralTable'; import IntegralsPreferences from './IntegralsPreferences'; +import { useActiveSpectrumIntegralsViewState } from '../../hooks/useActiveSpectrumIntegralsViewState'; const style = css` .sum-button { @@ -31,6 +32,10 @@ const style = css` align-items: center; justify-content: center; } + + button { + margin-right: 2px; + } `; export interface IntegralPanelInnerProps { @@ -49,10 +54,19 @@ function IntegralPanelInner({ const [filterIsActive, setFilterIsActive] = useState(false); const dispatch = useDispatch(); + const { showIntegralsValues } = useActiveSpectrumIntegralsViewState(); + const modal = useModal(); const [isFlipped, setFlipStatus] = useState(false); const settingRef = useRef(); + function handleShowIntegralsValues() { + dispatch({ + type: 'TOGGLE_INTEGRALS_VIEW_PROPERTY', + payload: { key: 'showIntegralsValues' }, + }); + } + const yesHandler = useCallback(() => { dispatch({ type: 'DELETE_INTEGRAL', payload: {} }); }, [dispatch]); @@ -180,6 +194,20 @@ function IntegralPanelInner({ > + + + )} {isFlipped && ( diff --git a/src/component/panels/IntegralsPanel/IntegralsPreferences.tsx b/src/component/panels/IntegralsPanel/IntegralsPreferences.tsx index ed2beae5b..d042c33cd 100644 --- a/src/component/panels/IntegralsPanel/IntegralsPreferences.tsx +++ b/src/component/panels/IntegralsPanel/IntegralsPreferences.tsx @@ -10,7 +10,7 @@ import { import { usePreferences } from '../../context/PreferencesContext'; import Label from '../../elements/Label'; -import FormikColorInput from '../../elements/formik/FormikColorInput'; +import FormikColorPickerDropdown from '../../elements/formik/FormikColorPickerDropdown'; import { formatFieldLabelStyle } from '../../elements/formik/FormikColumnFormatField'; import FormikInput from '../../elements/formik/FormikInput'; import useNucleus from '../../hooks/useNucleus'; @@ -109,7 +109,7 @@ function IntegralsPreferences(props, ref) {