Skip to content

Commit

Permalink
Merge branch 'main' into fix-annoations
Browse files Browse the repository at this point in the history
  • Loading branch information
axelboc authored Aug 21, 2023
2 parents 7094462 + 8fcf777 commit 851d010
Show file tree
Hide file tree
Showing 13 changed files with 262 additions and 176 deletions.
208 changes: 114 additions & 94 deletions apps/storybook/src/Annotation.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ import { formatCoord } from './utils';

const meta = {
title: 'Building Blocks/Annotation',
parameters: { layout: 'fullscreen' },
component: Annotation,
parameters: {
layout: 'fullscreen',
controls: { sort: 'requiredFirst' },
},
decorators: [
(Story) => (
<VisCanvas
Expand All @@ -28,94 +32,124 @@ const meta = {
),
FillHeight,
],
} satisfies Meta;
args: {
overflowCanvas: false,
scaleOnZoom: false,
center: false,
},
} satisfies Meta<typeof Annotation>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default = {
render: () => (
<>
<Annotation x={10} y={16}>
HTML annotation positioned at (10, 16)
</Annotation>
<Annotation
x={10}
y={6}
center
style={{
width: 180,
textAlign: 'center',
}}
>
Another annotation, <strong>centred</strong> on (10, 6)
</Annotation>
<Annotation
x={25}
y={10}
style={{
display: 'flex',
alignItems: 'center',
width: 320,
height: 75,
fontSize: '0.875rem',
textAlign: 'center',
}}
>
<>
<p
style={{
flex: '1 1 0%',
margin: 0,
padding: '0.5rem',
border: '10px solid pink',
}}
>
Annotations don't have to contain just text. You can also draw
shapes with CSS and SVG.
</p>
<svg
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
overflow: 'visible',
}}
>
<rect
width="100%"
height="100%"
fill="none"
stroke="darksalmon"
strokeWidth={5}
/>
</svg>
</>
render: (args) => {
const { x, y, overflowCanvas, scaleOnZoom, center, style } = args;

const features = [
overflowCanvas ? 'overflows the canvas' : '',
scaleOnZoom ? 'scales on zoom' : '',
]
.filter((str) => str.length > 0)
.join(' and ');

return (
<Annotation {...args}>
<p
style={{
margin: 0,
backgroundColor: 'rgba(0, 255, 0, 0.3)',
...style,
}}
>
Annotation {center ? 'centered on' : 'positioned at'} ({x}, {y})
{features && <> that {features}</>}
</p>
<svg
width="30"
height="30"
fill="transparent"
stroke="lightsalmon"
strokeWidth={3}
style={{
position: 'absolute',
top: center ? '50%' : 0,
left: center ? '50%' : 0,
transform: 'translate(-50%, -50%)',
zIndex: -1,
overflow: 'visible',
}}
>
<line x1="0%" x2="100%" y1="50%" y2="50%" />
<line x1="50%" x2="50%" y1="0%" y2="100%" />
</svg>
</Annotation>
</>
),
);
},
args: {
x: 10,
y: 16,
},
} satisfies Story;

export const WithZoom = {
render: () => (
<>
<Annotation x={10} y={16} scaleOnZoom style={{ width: 230 }}>
HTML annotation at (10, 16) that scales with zoom.
</Annotation>
<Annotation
x={25}
y={10}
scaleOnZoom
center
style={{ width: 320, textAlign: 'center' }}
>
Another annotation that scales with zoom but this time{' '}
<strong>centred</strong> on (25, 10)
</Annotation>
</>
export const OverflowCanvas = {
...Default,
args: {
x: 6,
y: 16,
overflowCanvas: true,
},
} satisfies Story;

export const Centered = {
...Default,
args: {
x: 5,
y: 14,
center: true,
},
} satisfies Story;

export const ScaleOnZoom = {
...Default,
args: {
x: 10,
y: 16,
scaleOnZoom: true,
},
} satisfies Story;

export const ScaleOnZoomCentered = {
...Default,
args: {
x: 10,
y: 16,
scaleOnZoom: true,
center: true,
},
} satisfies Story;

export const FollowPointer = {
render: (args) => (
<PointerTracker>
{(x, y) => (
<Annotation
{...args}
x={x + 0.5} // slight offset from pointer
y={y - 0.5}
style={{ whiteSpace: 'nowrap' }}
>{`x=${formatCoord(x)}, y=${formatCoord(y)}`}</Annotation>
)}
</PointerTracker>
),
args: {
x: 0,
y: 0,
},
argTypes: {
x: { control: false },
y: { control: false },
},
} satisfies Story;

function PointerTracker(props: {
Expand All @@ -137,17 +171,3 @@ function PointerTracker(props: {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{coords ? children(...coords) : null}</>;
}

export const FollowPointer = {
render: () => (
<PointerTracker>
{(x, y) => (
<Annotation
x={x + 0.5} // slight offset from pointer
y={y - 0.5}
style={{ whiteSpace: 'nowrap' }}
>{`x=${formatCoord(x)}, y=${formatCoord(y)}`}</Annotation>
)}
</PointerTracker>
),
} satisfies Story;
48 changes: 48 additions & 0 deletions apps/storybook/src/Utilities.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,54 @@ const pt = useCameraState(
);
```

#### useInteraction

Register an interaction. You must provide a unique ID that is not used by other interactions inside the current `VisCanvas` (pan, zoom, etc.)

The hook returns a function, conventionally named `shouldInteract`, that allows testing if a given mouse event (`PointerEvent` or `WheelEvent`)
is allowed to start or continue the interaction. It checks whether the event was triggered with the same mouse button and modifier key(s)
with which the interaction was registered and ensures that there is no interaction that is better suited to handle this event.

```ts
useInteraction(
id: string,
config: InteractionConfig,
): (event: MouseEvent) => boolean

const shouldInteract = useInteraction('MyInteraction', {
button: MouseButton.Left,
modifierKey: 'Control',
})

const onPointerDown = useCallback((evt: CanvasEvent<PointerEvent>) => {
if (shouldInteract(evt.sourceEvent)) {
/* ... */
}
},
[shouldInteract]);

useCanvasEvents({ onPointerDown }};
```
#### useModifierKeyPressed
Keeps track of the pressed state of one or more modifier keys.
The hook removes the need for a mouse event to be fired to know the state of the given modifier keys, which allows reacting to the user releasing
a key at any time, even when the mouse is immobile.
```ts
useModifierKeyPressed(modifierKey?: ModifierKey | ModifierKey[]): boolean

const isModifierKeyPressed = useModifierKeyPressed('Shift');

const onPointerMove = useCallback((evt: CanvasEvent<PointerEvent>) => {
if (isModifierKeyPressed) {
return;
}
}, [isModifierKeyPressed]);
```
### Mock data
The library exposes a utility function to retrieve a mock entity's metadata and a mock dataset's value as ndarray for testing purposes.
Expand Down
33 changes: 21 additions & 12 deletions packages/lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,28 +118,26 @@ export {
export { useVisDomain, useSafeDomain } from './vis/heatmap/hooks';

export { scaleGamma } from './vis/scaleGamma';
export { useCanvasEvents } from './interactions/hooks';

export {
useCanvasEvents,
useInteraction,
useModifierKeyPressed,
} from './interactions/hooks';
export { default as Box } from './interactions/box';

// Constants
export { COLOR_SCALE_TYPES, AXIS_SCALE_TYPES } from '@h5web/shared';

// Models
export { INTERPOLATORS } from './vis/heatmap/interpolators';

// Enums
export { ScaleType } from '@h5web/shared';
export { CurveType, GlyphType } from './vis/line/models';
export { ImageType } from './vis/rgb/models';
export { Notation } from './vis/matrix/models';

export type {
InteractionInfo,
ModifierKey,
Selection,
Rect,
CanvasEvent,
CanvasEventCallbacks,
} from './interactions/models';
export { MouseButton } from './interactions/models';

// Models
export type {
Domain,
VisibleDomains,
Expand Down Expand Up @@ -168,6 +166,17 @@ export type {
export type { D3Interpolator, ColorMap } from './vis/heatmap/models';
export type { ScatterAxisParams } from './vis/scatter/models';

export type {
ModifierKey,
Rect,
Selection,
CanvasEvent,
CanvasEventCallbacks,
InteractionInfo,
InteractionConfig,
CommonInteractionProps,
} from './interactions/models';

// Mock data and utilities
export {
mockMetadata,
Expand Down
Loading

0 comments on commit 851d010

Please sign in to comment.