Skip to content

Commit

Permalink
feat: inset UI
Browse files Browse the repository at this point in the history
  • Loading branch information
hamed-musallam committed Jan 28, 2025
1 parent b6f3655 commit 0bd4779
Show file tree
Hide file tree
Showing 12 changed files with 314 additions and 15 deletions.
6 changes: 5 additions & 1 deletion src/component/1d-2d/components/ClipPathContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { useInsetOptions } from '../../1d/inset/InsetProvider.js';
import { useChartData } from '../../context/ChartContext.js';

export function ClipPathContainer({ children }) {
const { displayerKey } = useChartData();
const { id: insetKey = 'primary' } = useInsetOptions() || {};

return <g clipPath={`url(#${displayerKey}clip-chart)`}>{children}</g>;
return (
<g clipPath={`url(#${displayerKey}clip-chart-${insetKey})`}>{children}</g>
);
}
9 changes: 6 additions & 3 deletions src/component/1d-2d/components/SVGRootContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
import type { ReactNode } from 'react';

import { useInsetOptions } from '../../1d/inset/InsetProvider.js';
import { useChartData } from '../../context/ChartContext.js';
import { usePreferences } from '../../context/PreferencesContext.js';

interface SVGRootContainerProps {
children: ReactNode;
enableBoxBorder?: boolean;
id?: string;
}

export function SVGRootContainer(props: SVGRootContainerProps) {
const { children, enableBoxBorder = false } = props;
const { children, enableBoxBorder = false, id = 'nmrSVG' } = props;

const {
current: {
general: { spectraRendering },
},
} = usePreferences();
const { width, height, margin, displayerKey } = useChartData();
const { id: insetKey = 'primary' } = useInsetOptions() || {};

const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;

return (
<svg
id="nmrSVG"
id={id}
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
Expand All @@ -34,7 +37,7 @@ export function SVGRootContainer(props: SVGRootContainerProps) {
}}
>
<defs>
<clipPath id={`${displayerKey}clip-chart`}>
<clipPath id={`${displayerKey}clip-chart-${insetKey}`}>
<rect
width={innerWidth}
height={innerHeight}
Expand Down
7 changes: 7 additions & 0 deletions src/component/1d/FooterBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { useActiveSpectrum } from '../hooks/useActiveSpectrum.js';
import { useFormatNumberByNucleus } from '../hooks/useFormatNumberByNucleus.js';
import useSpectrum from '../hooks/useSpectrum.js';

import { useInsetOptions } from './inset/InsetProvider.js';

const FlexInfoItem = styled(InfoItem)`
align-items: center;
`;
Expand Down Expand Up @@ -52,6 +54,11 @@ function FooterBannerInner({
const { scaleX } = useScaleChecked();

const format = useFormatNumberByNucleus(activeTab);
const isInset = useInsetOptions();

if (isInset) {
return null;
}

if (
!position ||
Expand Down
6 changes: 4 additions & 2 deletions src/component/1d/Line.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import useXYReduce, { XYReducerDomainAxis } from '../hooks/useXYReduce.js';
import { PathBuilder } from '../utility/PathBuilder.js';
import { parseColor } from '../utility/parseColor.js';

import { useInsetOptions } from './inset/InsetProvider.js';

interface LineProps {
data?: {
x: Float64Array;
Expand All @@ -23,6 +25,7 @@ function Line({ data, id, display, index }: LineProps) {
const { scaleX, scaleY, shiftY } = useScaleChecked();
const xyReduce = useXYReduce(XYReducerDomainAxis.XAxis);
const { opacity } = useActiveSpectrumStyleOptions(id);
const { id: insetKey = 'primary' } = useInsetOptions() || {};

const paths = useMemo(() => {
const _scaleX = scaleX();
Expand All @@ -46,10 +49,9 @@ function Line({ data, id, display, index }: LineProps) {
const { color: stroke, opacity: strokeOpacity } = parseColor(
display?.color || 'black',
);

return (
<path
id={id}
id={`${id}-${insetKey}`}
className="line"
data-testid="spectrum-line"
stroke={stroke}
Expand Down
8 changes: 6 additions & 2 deletions src/component/1d/LinesSeries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useSetActiveSpectrumAction } from '../hooks/useSetActiveSpectrumAction.
import { useVerticalAlign } from '../hooks/useVerticalAlign.js';

import Line from './Line.js';
import { useInsetOptions } from './inset/InsetProvider.js';
import { SPECTRA_BOTTOM_MARGIN } from './utilities/scale.js';

const BOX_SIZE = 10;
Expand All @@ -29,7 +30,7 @@ function LinesSeries() {
const spectra = (data?.filter(
(d) => isSpectrum1D(d) && d.display.isVisible && xDomains[d.id],
) || []) as Spectrum1D[];

const { id: insetKey = 'primary' } = useInsetOptions() || {};
return (
<g className="spectra">
{spectra.map((d, i) => (
Expand All @@ -39,7 +40,10 @@ function LinesSeries() {
</g>
))}
{activeSpectra?.map((activeSpectrum) => (
<use key={activeSpectrum.id} href={`#${activeSpectrum.id}`} />
<use
key={activeSpectrum.id}
href={`#${activeSpectrum.id}-${insetKey}`}
/>
))}
</g>
);
Expand Down
17 changes: 14 additions & 3 deletions src/component/1d/XAxis1D.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { useChartData } from '../context/ChartContext.js';
import { useScale } from '../context/ScaleContext.js';
import { AxisGroup } from '../elements/AxisGroup.js';

import { useInsetOptions } from './inset/InsetProvider.js';

const GridGroup = styled.g`
user-select: none;
Expand Down Expand Up @@ -33,6 +35,7 @@ export function XAxis1D(props: XAxisProps) {
const { show = true, showGrid = false, label: labelProp } = props;
const { xDomain, height, width, margin, mode } = useChartData();
const { scaleX } = useScale();
const isInset = useInsetOptions();

const refAxis = useRef<SVGGElement>(null);
const refGrid = useRef<SVGGElement>(null);
Expand Down Expand Up @@ -72,9 +75,17 @@ export function XAxis1D(props: XAxisProps) {
transform={`translate(0,${height - margin.bottom})`}
ref={refAxis}
>
<text fill="#000" x={width - 10} y="30" dy="0.70em" textAnchor="end">
{label}
</text>
{!isInset && (
<text
fill="#000"
x={width - 10}
y="30"
dy="0.70em"
textAnchor="end"
>
{label}
</text>
)}
</AxisGroup>
)}
{showGrid && (
Expand Down
129 changes: 129 additions & 0 deletions src/component/1d/inset/DraggableInset.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import styled from '@emotion/styled';
import { useEffect, useState } from 'react';
import { BsArrowsMove } from 'react-icons/bs';
import { FaTimes } from 'react-icons/fa';
import { Rnd } from 'react-rnd';

import { useGlobal } from '../../context/GlobalContext.js';
import { ActionsButtonsPopover } from '../../elements/ActionsButtonsPopover.js';
import type { ActionsButtonsPopoverProps } from '../../elements/ActionsButtonsPopover.js';
import { Viewer1D } from '../Viewer1D.js';

import { InsetProvider } from './InsetProvider.js';

import type { Inset } from './index.js';

const AUTO_CROP_MARGIN = 30;

interface InsetBounding {
x: number;
y: number;
width: number;
height: number;
}

const ReactRnd = styled(Rnd)`
border: 1px solid transparent;
&:hover {
border: 1px solid #ebecf1;
background-color: white;
button {
visibility: visible;
}
}
`;

export function DraggableInset(props: Pick<Inset, 'id' | 'bounding'>) {
const { id, bounding: externalBounding } = props;
const { viewerRef } = useGlobal();
const [bounding, setBounding] = useState<InsetBounding>(externalBounding);
const [isMoveActive, setIsMoveActive] = useState(false);

useEffect(() => {
setBounding({ ...externalBounding });
}, [externalBounding]);

// eslint-disable-next-line unicorn/consistent-function-scoping, @typescript-eslint/no-empty-function
function handleRemove() {}

Check failure on line 49 in src/component/1d/inset/DraggableInset.tsx

View workflow job for this annotation

GitHub Actions / nodejs / lint-eslint

'handleRemove' is defined but never used

function handleResize(
internalBounding: Pick<InsetBounding, 'height' | 'width'>,
) {
const { width, height } = externalBounding;
internalBounding.width += width;
internalBounding.height += height;
setBounding((prevBounding) => ({ ...prevBounding, ...internalBounding }));
}

function handleDrag(internalBounding: Pick<InsetBounding, 'x' | 'y'>) {
setBounding((prevBounding) => ({ ...prevBounding, ...internalBounding }));
}

if (!viewerRef) return null;

const { width, height, x, y } = bounding;

const actionsButtons: ActionsButtonsPopoverProps['buttons'] = [
{
icon: <BsArrowsMove />,

intent: 'none',
title: 'Move information block',
style: { cursor: 'move' },
className: 'handle',
},
{
icon: <FaTimes />,
intent: 'danger',
title: 'Hide information block',
},
];

return (
<ReactRnd
default={bounding}
minWidth={90 + AUTO_CROP_MARGIN * 2}
minHeight={100 + AUTO_CROP_MARGIN * 2}
dragHandleClassName="handle"
enableUserSelectHack={false}
bounds={viewerRef}
style={{ zIndex: 1 }}
onDragStart={() => setIsMoveActive(true)}
onResize={(e, dir, eRef, { width, height }) =>
handleResize({ width, height })
}
// onResizeStop={(e, dir, eRef, { width, height }) =>
// }
onDrag={(e, { x, y }) => {
handleDrag({ x, y });
}}
onDragStop={(e, { x, y }) => {
handleDrag({ x, y });
setIsMoveActive(false);
}}
resizeHandleWrapperStyle={{ backgroundColor: 'white' }}
>
<ActionsButtonsPopover
buttons={actionsButtons}
fill
positioningStrategy="fixed"
position="top-left"
direction="row"
targetProps={{ style: { width: '100%', height: '100%' } }}
space={2}
{...(isMoveActive && { isOpen: true })}
modifiers={{
offset: {
data: { x, y },
},
}}
>
<InsetProvider id={id} width={width} height={height}>
<Viewer1D />
</InsetProvider>
</ActionsButtonsPopover>
</ReactRnd>
);
}
40 changes: 40 additions & 0 deletions src/component/1d/inset/InsetProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { ReactNode } from 'react';
import { createContext, useContext, useMemo } from 'react';

export interface InsetPagContextProps {
width: number;
height: number;
id: string;
}

const InsetContext = createContext<InsetPagContextProps | null>(null);

export function useInsetOptions() {
return useContext(InsetContext);
}

interface InsetProviderProps extends InsetPagContextProps {
children: ReactNode;
}

export function InsetProvider(props: InsetProviderProps) {
const { children, width, height, id = 'primary' } = props;

const state = useMemo(() => {
return {
width,
height,
margin: {
top: 10,
right: 10,
bottom: 30,
left: 10,
},
id,
};
}, [height, id, width]);

return (
<InsetContext.Provider value={state}>{children}</InsetContext.Provider>
);
}
49 changes: 49 additions & 0 deletions src/component/1d/inset/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { DraggableInset } from './DraggableInset.js';

export interface InsetBounding {
x: number;
y: number;
width: number;
height: number;
}

export interface Inset {
id: string;
bounding: InsetBounding;
spectraIds: string[];
xDomain: number[];
yDomain: number[];
}

const insets: Inset[] = [
{
id: 'inset1',
bounding: { x: 0, y: 0, width: 400, height: 200 },
spectraIds: [],
xDomain: [1, 12],
yDomain: [0, 500],
},
{
id: 'inset2',
bounding: { x: 150, y: 0, width: 300, height: 150 },
spectraIds: [],
xDomain: [1, 12],
yDomain: [0, 500],
},
];

export function Insets() {
return (
<>
{insets.map((inset) => {
return (
<DraggableInset
key={inset.id}
id={inset.id}
bounding={inset.bounding}
/>
);
})}
</>
);
}
Loading

0 comments on commit 0bd4779

Please sign in to comment.