Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added tooltip for TiledHeatmapMesh #1193

Merged
merged 1 commit into from
Oct 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 47 additions & 12 deletions apps/storybook/src/TiledHeatmapMesh.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,26 @@ class MandelbrotTilesApi extends TilesApi {
}
}

class CheckboardTilesApi extends TilesApi {
loichuder marked this conversation as resolved.
Show resolved Hide resolved
public constructor(size: Size, tileSize: Size) {
super(tileSize, getLayerSizes(size, tileSize));
}

public get(layer: number, offset: Vector2): NdArray<Uint8Array> {
const layerSize = this.layerSizes[layer];
// Clip slice to size of the level
const width = clamp(layerSize.width - offset.x, 0, this.tileSize.width);
const height = clamp(layerSize.height - offset.y, 0, this.tileSize.height);

const value = Math.abs(
(Math.floor(offset.x / this.tileSize.width) % 2) -
(Math.floor(offset.y / this.tileSize.height) % 2)
);
const arr = Uint8Array.from({ length: height * width }, () => value);
return ndarray(arr, [height, width]);
}
}

interface TiledHeatmapStoryProps extends TiledHeatmapMeshProps {
abscissaConfig: AxisConfig;
ordinateConfig: AxisConfig;
Expand All @@ -152,11 +172,7 @@ const Template: Story<TiledHeatmapStoryProps> = (args) => {
<Zoom />
<SelectToZoom keepRatio modifierKey="Control" />
<ResetZoomButton />
<group
scale={[abscissaConfig.flip ? -1 : 1, ordinateConfig.flip ? -1 : 1, 1]}
>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The flip is now handled at the layer level to avoid flipping the TiledTooltipMesh

<TiledHeatmapMesh {...tiledHeatmapProps} />
</group>
<TiledHeatmapMesh {...tiledHeatmapProps} />
</VisCanvas>
);
};
Expand All @@ -167,6 +183,16 @@ const defaultApi = new MandelbrotTilesApi(
[-2, 1],
[-1.5, 1.5]
);
const halfMandelbrotApi = new MandelbrotTilesApi(
{ width: 1e9, height: 5e8 },
{ width: 256, height: 128 },
[-2, 1],
[0, 1.5]
);
const checkboardApi = new CheckboardTilesApi(
{ width: 1e9, height: 1e9 },
{ width: 128, height: 128 }
);

export const Default = Template.bind({});
Default.args = {
Expand All @@ -183,13 +209,6 @@ Default.args = {
},
};

const halfMandelbrotApi = new MandelbrotTilesApi(
{ width: 1e9, height: 5e8 },
{ width: 256, height: 128 },
[-2, 1],
[0, 1.5]
);

export const AxisValues = Template.bind({});
AxisValues.args = {
api: halfMandelbrotApi,
Expand Down Expand Up @@ -292,6 +311,21 @@ WithTransforms.args = {
},
};

export const Checkboard = Template.bind({});
Checkboard.args = {
api: checkboardApi,
abscissaConfig: {
visDomain: [0, checkboardApi.baseLayerSize.width],
isIndexAxis: true,
showGrid: false,
},
ordinateConfig: {
visDomain: [0, checkboardApi.baseLayerSize.height],
isIndexAxis: true,
showGrid: false,
},
};

export default {
title: 'Experimental/TiledHeatmapMesh',
component: TiledHeatmapMesh,
Expand All @@ -304,6 +338,7 @@ export default {
scaleType: ScaleType.Linear,
displayLowerResolutions: true,
qualityFactor: 1,
showTooltip: true,
},
argTypes: {
scaleType: {
Expand Down
3 changes: 2 additions & 1 deletion packages/lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@
"react-measure": "2.5.2",
"react-slider": "2.0.4",
"react-use": "17.4.0",
"react-window": "1.8.7"
"react-window": "1.8.7",
"zustand": "4.1.1"
},
"devDependencies": {
"@h5web/shared": "workspace:*",
Expand Down
10 changes: 7 additions & 3 deletions packages/lib/src/vis/shared/TooltipMesh.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@ import { useTooltip } from '@visx/tooltip';
import { useCallback } from 'react';
import type { ReactElement } from 'react';

import type { Coords } from '../models';
import type { Coords, Size } from '../models';
import { useAxisSystemContext } from './AxisSystemProvider';
import TooltipOverlay from './TooltipOverlay';
import VisMesh from './VisMesh';

interface Props {
size?: Size;
guides?: 'horizontal' | 'vertical' | 'both';
renderTooltip: (x: number, y: number) => ReactElement | undefined;
}

function TooltipMesh(props: Props) {
const { guides, renderTooltip } = props;
const { guides, renderTooltip, size } = props;

const { width, height } = useThree((state) => state.size);

Expand Down Expand Up @@ -79,7 +80,10 @@ function TooltipMesh(props: Props) {

return (
<>
<VisMesh {...{ onPointerMove, onPointerOut, onPointerDown, onPointerUp }}>
<VisMesh
{...{ onPointerMove, onPointerOut, onPointerDown, onPointerUp }}
size={size}
>
<meshBasicMaterial opacity={0} transparent />
</VisMesh>
<TooltipOverlay
Expand Down
19 changes: 17 additions & 2 deletions packages/lib/src/vis/tiles/Tile.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,39 @@
import type { ThreeEvent } from '@react-three/fiber';
import { throttle } from 'lodash';
import { memo } from 'react';
import { Vector2 } from 'three';
import type { TextureFilter } from 'three';

import HeatmapMesh from '../heatmap/HeatmapMesh';
import type { TilesApi } from './api';
import type { ColorMapProps } from './models';
import type { ColorMapProps, TileArray } from './models';

interface Props extends ColorMapProps {
api: TilesApi;
layer: number;
x: number;
y: number;
magFilter: TextureFilter;
onPointerMove?: (e: ThreeEvent<MouseEvent>, array: TileArray) => void;
}

function Tile(props: Props) {
const { api, layer, x, y, magFilter, ...colorMapProps } = props;
const { api, layer, x, y, magFilter, onPointerMove, ...colorMapProps } =
props;

const array = api.get(layer, new Vector2(x, y));
const [height, width] = array.shape;

const handlePointerMove =
onPointerMove &&
throttle(
(e: ThreeEvent<MouseEvent>) => {
onPointerMove(e, array);
},
200,
{ trailing: false }
loichuder marked this conversation as resolved.
Show resolved Hide resolved
);

return (
<group
position={[x + width / 2, y + height / 2, 0]} // Tile center
Expand All @@ -29,6 +43,7 @@ function Tile(props: Props) {
{...colorMapProps}
magFilter={magFilter}
size={{ width, height }}
onPointerMove={handlePointerMove}
/>
</group>
);
Expand Down
55 changes: 33 additions & 22 deletions packages/lib/src/vis/tiles/TiledHeatmapMesh.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,23 @@ import { useCameraState } from '../hooks';
import type { Size } from '../models';
import { useAxisSystemContext } from '../shared/AxisSystemProvider';
import TiledLayer from './TiledLayer';
import TiledTooltipMesh from './TiledTooltipMesh';
import type { TilesApi } from './api';
import { useLayerScales, useTooltipOnMoveHandler } from './hooks';
import type { ColorMapProps } from './models';
import {
getObject3DVisibleBox,
getObject3DPixelSize,
getNdcToObject3DMatrix,
scaleToLayer,
getObject3DPixelSize,
getObject3DVisibleBox,
scaleBox,
} from './utils';

interface Props extends ColorMapProps {
api: TilesApi;
displayLowerResolutions?: boolean;
qualityFactor?: number;
size?: Size;
showTooltip?: boolean;
}

function TiledHeatmapMesh(props: Props) {
Expand All @@ -30,6 +33,7 @@ function TiledHeatmapMesh(props: Props) {
displayLowerResolutions = true,
qualityFactor = 1, // 0: Lower quality, less fetch; 1: Best quality
size,
showTooltip,
...colorMapProps
} = props;
const { baseLayerIndex, baseLayerSize } = api;
Expand All @@ -46,7 +50,8 @@ function TiledHeatmapMesh(props: Props) {
);
const visibleBox = getObject3DVisibleBox(ndcToLocalMatrix);

const bounds = scaleToLayer(visibleBox, baseLayerSize, meshSize);
const { xScale, yScale } = useLayerScales(baseLayerSize, meshSize);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scales are now computed by a hook outside of scaleToLayer (renamed scaleBox) to be able to access the AxisContext and take the flip in account

const bounds = scaleBox(visibleBox, xScale, yScale);

let layers: number[] = [];
if (!bounds.isEmpty()) {
Expand Down Expand Up @@ -74,25 +79,31 @@ function TiledHeatmapMesh(props: Props) {

const { colorMap, invertColorMap = false } = colorMapProps;

const onPointerMove = useTooltipOnMoveHandler();

return (
<group ref={groupRef}>
<mesh position={[0, 0, -0.1]}>
<planeGeometry args={[meshSize.width, meshSize.height]} />
<meshBasicMaterial
color={getInterpolator(colorMap, invertColorMap)(0)}
/>
</mesh>
{layers.map((layer) => (
<TiledLayer
key={layer}
api={api}
layer={layer}
meshSize={meshSize}
visibleBox={visibleBox}
{...colorMapProps}
/>
))}
</group>
<>
<group ref={groupRef}>
<mesh position={[0, 0, -0.1]}>
<planeGeometry args={[meshSize.width, meshSize.height]} />
<meshBasicMaterial
color={getInterpolator(colorMap, invertColorMap)(0)}
/>
</mesh>
{layers.map((layer, i) => (
<TiledLayer
key={layer}
api={api}
layer={layer}
meshSize={meshSize}
visibleBox={visibleBox}
onPointerMove={i === layers.length - 1 ? onPointerMove : undefined} // Attach tooltip handler only to the top layer
{...colorMapProps}
/>
))}
</group>
{showTooltip && <TiledTooltipMesh size={meshSize} />}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to make the size of TooltipMesh match the size of the TiledHeatmap

</>
);
}

Expand Down
62 changes: 40 additions & 22 deletions packages/lib/src/vis/tiles/TiledLayer.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,75 @@
import type { ThreeEvent } from '@react-three/fiber';
import { Suspense } from 'react';
import { LinearFilter, NearestFilter, Vector2 } from 'three';
import type { Box3 } from 'three';

import type { Size } from '../models';
import { useAxisSystemContext } from '../shared/AxisSystemProvider';
import Tile from './Tile';
import type { TilesApi } from './api';
import type { ColorMapProps } from './models';
import { getTileOffsets, scaleToLayer, sortTilesByDistanceTo } from './utils';
import { useLayerScales } from './hooks';
import type { ColorMapProps, TileArray } from './models';
import { getTileOffsets, scaleBox, sortTilesByDistanceTo } from './utils';

interface Props extends ColorMapProps {
api: TilesApi;
layer: number;
meshSize: Size;
visibleBox: Box3;
onPointerMove?: (e: ThreeEvent<MouseEvent>, array: TileArray) => void;
}

function TiledLayer(props: Props) {
const { api, layer, meshSize, visibleBox, ...colorMapProps } = props;
const { api, layer, meshSize, visibleBox, onPointerMove, ...colorMapProps } =
props;

const { baseLayerIndex, numLayers, tileSize } = api;
const layerSize = api.layerSizes[layer];
const { xScale, yScale } = useLayerScales(layerSize, meshSize);
const { abscissaConfig, ordinateConfig } = useAxisSystemContext();

if (visibleBox.isEmpty()) {
return null;
}

const bounds = scaleToLayer(visibleBox, layerSize, meshSize);
const bounds = scaleBox(visibleBox, xScale, yScale);
const tileOffsets = getTileOffsets(bounds, tileSize);
// Sort tiles from closest to vis center to farthest away
sortTilesByDistanceTo(tileOffsets, tileSize, bounds.getCenter(new Vector2()));

return (
// Transforms to use level of details layer array coordinates
<group
position={[-meshSize.width / 2, -meshSize.height / 2, layer / numLayers]}
scale={[
meshSize.width / layerSize.width,
meshSize.height / layerSize.height,
1,
]}
scale={[abscissaConfig.flip ? -1 : 1, ordinateConfig.flip ? -1 : 1, 1]}
>
{tileOffsets.map((offset) => (
<Suspense key={`${offset.x},${offset.y}`} fallback={null}>
<Tile
api={api}
layer={layer}
x={offset.x}
y={offset.y}
{...colorMapProps}
magFilter={layer === baseLayerIndex ? NearestFilter : LinearFilter}
/>
</Suspense>
))}
<group
position={[
-meshSize.width / 2,
-meshSize.height / 2,
layer / numLayers,
]}
scale={[
meshSize.width / layerSize.width,
meshSize.height / layerSize.height,
1,
]}
>
{tileOffsets.map((offset) => (
<Suspense key={`${offset.x},${offset.y}`} fallback={null}>
<Tile
api={api}
layer={layer}
x={offset.x}
y={offset.y}
{...colorMapProps}
magFilter={
layer === baseLayerIndex ? NearestFilter : LinearFilter
}
onPointerMove={onPointerMove}
/>
</Suspense>
))}
</group>
</group>
);
}
Expand Down
Loading