Skip to content

Commit

Permalink
feat: interactive positioning of peak labels
Browse files Browse the repository at this point in the history
  • Loading branch information
hamed-musallam committed Oct 16, 2024
1 parent fc33545 commit 0b9d3d8
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 52 deletions.
199 changes: 152 additions & 47 deletions src/component/1d/peaks/PeakAnnotationsSpreadMode.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import { memo } from 'react';
import { memo, useState } from 'react';
import { BsArrowsMove } from 'react-icons/bs';
import { useMeasure } from 'react-use';

import { useGlobal } from '../../context/GlobalContext';
import { usePreferences } from '../../context/PreferencesContext';
import {
ActionsButtonsPopover,
ActionsButtonsPopoverProps,
} from '../../elements/ActionsButtonsPopover';
import useDraggable from '../../elements/draggable/useDraggable';
import { useHighlight } from '../../highlight';
import { usePeaksLabelSettings } from '../../hooks/usePeaksLabelSettings';
import { Margin } from '../../reducer/Reducer';
import { formatNumber } from '../../utility/formatNumber';
import { resolve } from '../utilities/intersectionResolver';

Expand All @@ -17,13 +26,79 @@ import {
const notationWidth = 10;
const notationMargin = 2;

function PeakAnnotationsSpreadMode(
props: Omit<PeaksAnnotationsProps, 'xDomain'>,
) {
const { peaks, peaksSource, spectrumColor, displayerKey, peakFormat } = props;
function usePeaksPosition() {
const { viewerRef } = useGlobal();
const { dispatch } = usePreferences();
const { marginTop: originMarginTop } = usePeaksLabelSettings();
const [isDragActive, setIsMoveActive] = useState(false);
const [marginTop, setMarginTop] = useState<number>(originMarginTop);

const { onPointerDown } = useDraggable({
position: { x: 0, y: marginTop },
onChange: (dragEvent) => {
const { action, position } = dragEvent;
const yOffset = Math.round(position.y);
switch (action) {
case 'start': {
setMarginTop(yOffset);
setIsMoveActive(true);
break;
}
case 'move': {
setMarginTop(yOffset);

break;
}
case 'end':
dispatch({
type: 'CHANGE_PEAKS_LABEL_POSITION',
payload: {
marginTop: yOffset,
},
});
setIsMoveActive(false);
break;
default:
break;
}
},
parentElement: viewerRef,
});

return { marginTop, isDragActive, onPointerDown };
}

interface PeakAnnotationsSpreadModeProps
extends Omit<PeaksAnnotationsProps, 'xDomain'> {
height: number;
margin: Margin;
}

function PeakAnnotationsSpreadMode(props: PeakAnnotationsSpreadModeProps) {
const {
peaks,
peaksSource,
spectrumColor,
displayerKey,
peakFormat,
margin,
height,
} = props;
const [ref, boxSize] = useMeasure<SVGGElement>();
const { marginTop } = usePeaksLabelSettings();
const { marginTop, isDragActive, onPointerDown } = usePeaksPosition();

const actionsButtons: ActionsButtonsPopoverProps['buttons'] = [
{
icon: <BsArrowsMove />,
onPointerDown: (event) => {
event.stopPropagation();
onPointerDown(event);
},
intent: 'none',
title: 'Move peaks label vertically',
style: { cursor: 'move' },
},
];

const mapPeaks = resolve(peaks, {
key: 'scaleX',
Expand All @@ -32,50 +107,80 @@ function PeakAnnotationsSpreadMode(
groupMargin: 10,
});

const y = boxSize.height + marginTop;
const boxHeight = Math.round(boxSize.height);

let y = boxHeight + marginTop;

if (y + boxHeight > height - margin.bottom) {
y = height - margin.bottom - boxHeight;
}

if (marginTop < 0) {
y = boxHeight;
}
return (
<g
ref={ref}
className="peaks"
clipPath={`url(#${displayerKey}clip-chart-1d)`}
<ActionsButtonsPopover
targetTagName="g"
buttons={actionsButtons}
positioningStrategy="fixed"
position="top"
direction="row"
{...(isDragActive && { isOpen: true })}
modifiers={{
offset: {
data: { x: 0, y },
},
}}
>
<g
transform={`translate(0,${y})`}
style={{ visibility: boxSize.height > 0 ? 'visible' : 'hidden' }}
>
{mapPeaks.map((group) => {
return (
<g
key={group.meta.id}
transform={`translate(${group.meta.groupStartX},0)`}
>
{group.group.map((item, index) => {
const { id, x: value, scaleX, parentKeys } = item;
const startX = index * (notationWidth + notationMargin);
const x = scaleX - group.meta.groupStartX;
return (
<PeakAnnotation
key={id}
startX={startX}
x={x}
id={id}
parentKeys={parentKeys}
value={value}
format={peakFormat}
color={spectrumColor}
peakEditionFieldPosition={{
x: group.meta.groupStartX + startX,
y,
}}
peaksSource={peaksSource}
/>
);
})}
</g>
);
})}
<g className="peaks" clipPath={`url(#${displayerKey}clip-chart-1d)`}>
<g
transform={`translate(0,${y})`}
style={{ visibility: boxSize.height > 0 ? 'visible' : 'hidden' }}
>
<rect
data-no-export="true"
width="100%"
y={-boxHeight / 2}
height={boxHeight}
fill={isDragActive ? 'white' : 'transparent'}
opacity={isDragActive ? 0.9 : 0}
/>
<g ref={ref}>
{mapPeaks.map((group) => {
return (
<g
key={group.meta.id}
transform={`translate(${group.meta.groupStartX},0)`}
>
{group.group.map((item, index) => {
const { id, x: value, scaleX, parentKeys } = item;
const startX = index * (notationWidth + notationMargin);
const x = scaleX - group.meta.groupStartX;
return (
<PeakAnnotation
key={id}
startX={startX}
x={x}
id={id}
parentKeys={parentKeys}
value={value}
format={peakFormat}
color={spectrumColor}
peakEditionFieldPosition={{
x: group.meta.groupStartX + startX,
y,
}}
peaksSource={peaksSource}
/>
);
})}
</g>
);
})}
</g>
</g>
</g>
</g>
</ActionsButtonsPopover>
);
}

Expand Down
30 changes: 27 additions & 3 deletions src/component/1d/peaks/Peaks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useActiveSpectrumPeaksViewState } from '../../hooks/useActiveSpectrumPe
import { useActiveSpectrumRangesViewState } from '../../hooks/useActiveSpectrumRangesViewState';
import { usePanelPreferences } from '../../hooks/usePanelPreferences';
import useSpectrum from '../../hooks/useSpectrum';
import { Margin } from '../../reducer/Reducer';
import { useScaleX } from '../utilities/scale';

import PeakAnnotations from './PeakAnnotations';
Expand Down Expand Up @@ -104,11 +105,21 @@ function useMapPeaks(spectrum: Spectrum1D, filterBy: FilterPeaksBy) {
interface InnerPeaksProps extends BasePeaksProps {
spectrum: Spectrum1D;
mode: PeaksMode;
height: number;
margin: Margin;
}

function InnerPeaks(props: InnerPeaksProps) {
const { peaksSource, spectrum, mode, displayerKey, xDomain, peakFormat } =
props;
const {
peaksSource,
spectrum,
mode,
displayerKey,
xDomain,
height,
margin,
peakFormat,
} = props;

const peaks = useMapPeaks(
spectrum,
Expand All @@ -124,6 +135,8 @@ function InnerPeaks(props: InnerPeaksProps) {
spectrumColor={spectrum.display.color}
peakFormat={peakFormat}
displayerKey={displayerKey}
height={height}
margin={margin}
/>
);
}
Expand Down Expand Up @@ -153,6 +166,8 @@ export default function Peaks(props) {
},
displayerKey,
xDomain,
height,
margin,
} = useChartData();
const spectrum = useSpectrum(emptyData) as Spectrum1D;
const peaksViewState = useActiveSpectrumPeaksViewState();
Expand Down Expand Up @@ -196,7 +211,16 @@ export default function Peaks(props) {

return (
<MemoizedPeaksPanel
{...{ spectrum, mode, peaksSource, displayerKey, xDomain, peakFormat }}
{...{
spectrum,
mode,
peaksSource,
displayerKey,
xDomain,
peakFormat,
margin,
height,
}}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Draft } from 'immer';

import {
ChangePeaksLabelPositionAction,
PreferencesState,
} from '../preferencesReducer';
import { getActiveWorkspace } from '../utilities/getActiveWorkspace';

export function changePeaksLabelPosition(
draft: Draft<PreferencesState>,
action: ChangePeaksLabelPositionAction,
) {
const currentWorkspacePreferences = getActiveWorkspace(draft);
currentWorkspacePreferences.peaksLabel.marginTop = action.payload.marginTop;
}
15 changes: 13 additions & 2 deletions src/component/reducer/preferences/preferencesReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import {
setSpectraAnalysisPanelsPreferences,
} from './actions/analyzeSpectra';
import { applyGeneralPreferences } from './actions/applyGeneralPreferences';
import { changeExportSettings } from './actions/changeExportSettings';
import { changeInformationBlockPosition } from './actions/changeInformationBlockPosition';
import { changePeaksLabelPosition } from './actions/changePeaksLabelPosition';
import { changePrintPageSettings } from './actions/changePrintPageSettings';
import { initPreferences } from './actions/initPreferences';
import {
Expand All @@ -45,7 +47,6 @@ import { setVerticalSplitterPosition } from './actions/setVerticalSplitterPositi
import { setWorkspace } from './actions/setWorkspace';
import { toggleInformationBlock } from './actions/toggleInformationBlock';
import { mapWorkspaces } from './utilities/mapWorkspaces';
import { changeExportSettings } from './actions/changeExportSettings';

const LOCAL_STORAGE_SETTINGS_KEY = 'nmr-general-settings';

Expand Down Expand Up @@ -121,6 +122,7 @@ export type ChangeInformationBlockPosition = ActionType<
coordination: { x: number; y: number };
}
>;

export type ToggleInformationBlock = ActionType<
'TOGGLE_INFORMATION_BLOCK',
{
Expand All @@ -138,6 +140,12 @@ export type ChangeExportSettingsAction = ActionType<
options: ExportSettings;
}
>;
export type ChangePeaksLabelPositionAction = ActionType<
'CHANGE_PEAKS_LABEL_POSITION',
{
marginTop: number;
}
>;

type PreferencesActions =
| InitPreferencesAction
Expand All @@ -156,7 +164,8 @@ type PreferencesActions =
| ChangeInformationBlockPosition
| ToggleInformationBlock
| ChangePrintPageSettingsAction
| ChangeExportSettingsAction;
| ChangeExportSettingsAction
| ChangePeaksLabelPositionAction;

export const WORKSPACES: Array<{
key: NMRiumWorkspace;
Expand Down Expand Up @@ -311,6 +320,8 @@ function innerPreferencesReducer(
return changePrintPageSettings(draft, action);
case 'CHANGE_EXPORT_SETTINGS':
return changeExportSettings(draft, action);
case 'CHANGE_PEAKS_LABEL_POSITION':
return changePeaksLabelPosition(draft, action);

default:
return draft;
Expand Down

0 comments on commit 0b9d3d8

Please sign in to comment.