Skip to content

Commit

Permalink
Reimplement Tooltip by attaching handlers to Tiles
Browse files Browse the repository at this point in the history
  • Loading branch information
loichuder committed Sep 29, 2022
1 parent c34b298 commit 6143e56
Show file tree
Hide file tree
Showing 11 changed files with 398 additions and 71 deletions.
1 change: 0 additions & 1 deletion apps/storybook/src/TiledHeatmapMesh.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,6 @@ WithTransforms.args = {
showGrid: false,
flip: false,
},
showTooltip: false, // TODO: Tooltip not working with transforms
};

export const Checkboard = Template.bind({});
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 }
);

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
9 changes: 3 additions & 6 deletions packages/lib/src/vis/tiles/TiledHeatmapMesh.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ function TiledHeatmapMesh(props: Props) {
const bounds = scaleBox(visibleBox, xScale, yScale);

let layers: number[] = [];
let tooltipLayer: number | undefined;
if (!bounds.isEmpty()) {
const pixelSize = getObject3DPixelSize(ndcToLocalMatrix, canvasSize);
const dataPointsPerPixel = Math.max(
Expand All @@ -76,7 +75,6 @@ function TiledHeatmapMesh(props: Props) {
layers = displayLowerResolutions
? range(currentLayerIndex + 1)
: [currentLayerIndex];
tooltipLayer = currentLayerIndex;
}

const { colorMap, invertColorMap = false } = colorMapProps;
Expand All @@ -90,20 +88,19 @@ function TiledHeatmapMesh(props: Props) {
color={getInterpolator(colorMap, invertColorMap)(0)}
/>
</mesh>
{layers.map((layer) => (
{layers.map((layer, i) => (
<TiledLayer
key={layer}
api={api}
layer={layer}
meshSize={meshSize}
visibleBox={visibleBox}
topLayer={i === layers.length - 1}
{...colorMapProps}
/>
))}
</group>
{showTooltip && tooltipLayer !== undefined && (
<TiledTooltipMesh layer={tooltipLayer} api={api} meshSize={meshSize} />
)}
{showTooltip && <TiledTooltipMesh size={meshSize} />}
</>
);
}
Expand Down
30 changes: 26 additions & 4 deletions packages/lib/src/vis/tiles/TiledLayer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Suspense } from 'react';
import type { ThreeEvent } from '@react-three/fiber';
import { Suspense, useCallback } from 'react';
import { LinearFilter, NearestFilter, Vector2 } from 'three';
import type { Box3 } from 'three';

Expand All @@ -7,23 +8,43 @@ import { useAxisSystemContext } from '../shared/AxisSystemProvider';
import Tile from './Tile';
import type { TilesApi } from './api';
import { useLayerScales } from './hooks';
import type { ColorMapProps } from './models';
import type { ColorMapProps, TileArray } from './models';
import { useTooltipStore } from './store';
import { getTileOffsets, scaleBox, sortTilesByDistanceTo } from './utils';

interface Props extends ColorMapProps {
api: TilesApi;
layer: number;
meshSize: Size;
visibleBox: Box3;
topLayer?: boolean;
}

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

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

const onPointerMove = useCallback(
(e: ThreeEvent<MouseEvent>, array: TileArray) => {
const localVec = e.object.worldToLocal(e.unprojectedPoint.clone());
const dataVec = worldToData(e.unprojectedPoint.clone());
const [height, width] = array.shape;
const val = array.get(
Math.floor(localVec.y + height / 2),
Math.floor(localVec.x + width / 2)
);
setTooltipValue(dataVec.x, dataVec.y, val);
e.stopPropagation();
},
[setTooltipValue, worldToData]
);

if (visibleBox.isEmpty()) {
return null;
Expand Down Expand Up @@ -62,6 +83,7 @@ function TiledLayer(props: Props) {
magFilter={
layer === baseLayerIndex ? NearestFilter : LinearFilter
}
onPointerMove={topLayer ? onPointerMove : undefined}
/>
</Suspense>
))}
Expand Down
59 changes: 12 additions & 47 deletions packages/lib/src/vis/tiles/TiledTooltipMesh.tsx
Original file line number Diff line number Diff line change
@@ -1,70 +1,35 @@
import { formatTooltipVal } from '@h5web/shared';
import { Suspense } from 'react';
import { Vector2 } from 'three';

import type { Size } from '../models';
import { useAxisSystemContext } from '../shared/AxisSystemProvider';
import TooltipMesh from '../shared/TooltipMesh';
import type { TilesApi } from './api';
import { useLayerScales } from './hooks';
import { scaleFromLayerToTile } from './utils';
import { useTooltipStore } from './store';

interface Props {
layer: number;
api: TilesApi;
meshSize: Size;
size: Size;
}

function TiledTooltipMesh(props: Props) {
const { layer, api, meshSize } = props;

const layerSize = api.layerSizes[layer];

const { dataToWorld } = useAxisSystemContext();
const { xScale, yScale } = useLayerScales(layerSize, meshSize);

function dataToLayer(v: Vector2) {
const worldVec = dataToWorld(v);

return new Vector2(
Math.floor(xScale(worldVec.x)),
Math.floor(yScale(worldVec.y))
);
}
const { size } = props;
const val = useTooltipStore((state) => state.val);

return (
<TooltipMesh
renderTooltip={(x, y) => {
size={size}
renderTooltip={() => {
if (!val) {
return undefined;
}

const { x, y, v } = val;
return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
{`x=${formatTooltipVal(x)}, y=${formatTooltipVal(y)}`}
<strong>
<Suspense fallback={<>Loading...</>}>
<TooltipValue
api={api}
layerVec={dataToLayer(new Vector2(x, y))}
layer={layer}
/>
</Suspense>
</strong>
<strong>{formatTooltipVal(v)}</strong>
</div>
);
}}
/>
);
}

function TooltipValue(props: {
layerVec: Vector2;
api: TilesApi;
layer: number;
}) {
const { layerVec, api, layer } = props;

const { offset, x, y } = scaleFromLayerToTile(layerVec, api.tileSize);
const dataArray = api.get(layer, offset);

return <>{formatTooltipVal(dataArray.get(y, x))}</>;
}

export default TiledTooltipMesh;
8 changes: 2 additions & 6 deletions packages/lib/src/vis/tiles/api.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { NdArray } from 'ndarray';
import type { Vector2 } from 'three';

import type { TextureSafeTypedArray } from '../heatmap/models';
import type { Size } from '../models';
import type { TileArray } from './models';

export function getLayerSizes(
baseLayerSize: Size,
Expand Down Expand Up @@ -55,8 +54,5 @@ export abstract class TilesApi {
return this.layerSizes.length;
}

public abstract get(
layer: number,
offset: Vector2
): NdArray<TextureSafeTypedArray | Uint16Array>; // uint16 values are treated as half floats
public abstract get(layer: number, offset: Vector2): TileArray;
}
5 changes: 4 additions & 1 deletion packages/lib/src/vis/tiles/models.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Domain } from '@h5web/shared';
import type { NdArray } from 'ndarray';

import type { ColorMap } from '../heatmap/models';
import type { ColorMap, TextureSafeTypedArray } from '../heatmap/models';
import type { VisScaleType } from '../models';

export interface ColorMapProps {
Expand All @@ -9,3 +10,5 @@ export interface ColorMapProps {
colorMap: ColorMap;
invertColorMap?: boolean;
}

export type TileArray = NdArray<TextureSafeTypedArray | Uint16Array>; // uint16 values are treated as half floats
14 changes: 14 additions & 0 deletions packages/lib/src/vis/tiles/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import create from 'zustand';

interface TooltipStore {
val: { x: number; y: number; v: number } | undefined;
setTooltipValue: (x: number, y: number, v: number) => void;
}

export const useTooltipStore = create<TooltipStore>((set) => ({
val: undefined,
setTooltipValue: (x: number, y: number, v: number) =>
set(() => ({
val: { x, y, v },
})),
}));
Loading

0 comments on commit 6143e56

Please sign in to comment.