@pluot/react 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1066 -110
- package/dist-tsc/BrushOverlay.d.ts +31 -0
- package/dist-tsc/BrushOverlay.d.ts.map +1 -0
- package/dist-tsc/BrushOverlay.js +95 -0
- package/dist-tsc/Pluot.d.ts +2 -1
- package/dist-tsc/Pluot.d.ts.map +1 -1
- package/dist-tsc/Pluot.js +118 -22
- package/dist-tsc/Tooltip.d.ts +2 -1
- package/dist-tsc/Tooltip.d.ts.map +1 -1
- package/dist-tsc/Tooltip.js +15 -1
- package/dist-tsc/brush.d.ts +155 -0
- package/dist-tsc/brush.d.ts.map +1 -0
- package/dist-tsc/brush.js +312 -0
- package/dist-tsc/brush.test.d.ts +2 -0
- package/dist-tsc/brush.test.d.ts.map +1 -0
- package/dist-tsc/brush.test.js +487 -0
- package/dist-tsc/index.d.ts +3 -1
- package/dist-tsc/index.d.ts.map +1 -1
- package/dist-tsc/index.js +1 -0
- package/dist-tsc/types.d.ts +283 -0
- package/dist-tsc/types.d.ts.map +1 -0
- package/dist-tsc/types.js +11 -0
- package/dist-tsc/use-brush.d.ts +55 -0
- package/dist-tsc/use-brush.d.ts.map +1 -0
- package/dist-tsc/use-brush.js +361 -0
- package/package.json +5 -3
- package/src/BrushOverlay.tsx +258 -0
- package/src/{Pluot.jsx → Pluot.tsx} +200 -47
- package/src/{Tooltip.jsx → Tooltip.tsx} +19 -3
- package/src/brush.test.ts +590 -0
- package/src/brush.ts +435 -0
- package/src/index.ts +26 -0
- package/src/types.ts +412 -0
- package/src/use-brush.ts +505 -0
- package/src/index.js +0 -2
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
import React, { useLayoutEffect, useEffect, useEffectEvent, useRef, useState, useMemo, useReducer,
|
|
1
|
+
import React, { useLayoutEffect, useEffect, useEffectEvent, useRef, useState, useMemo, useReducer, useId, type CSSProperties } from "react";
|
|
2
2
|
import lzs from "lz-string";
|
|
3
|
-
import {
|
|
3
|
+
import { throttle } from "lodash-es";
|
|
4
4
|
import {
|
|
5
5
|
initialize, getIsWasmReady,
|
|
6
|
-
render_wasm, pick_wasm,
|
|
6
|
+
render_wasm, pick_wasm, brush_wasm,
|
|
7
7
|
normalizeStores, getStore,
|
|
8
|
-
getBounds, getCameraMatrixFromBounds,
|
|
9
8
|
checkWebGpuFeatureDetection,
|
|
10
9
|
onMouseMove2d, onWheel2d,
|
|
11
10
|
onMouseMove3d, onWheel3d,
|
|
11
|
+
type CameraMatrix,
|
|
12
12
|
} from '@pluot/core';
|
|
13
13
|
import { Tooltip } from "./Tooltip.js";
|
|
14
|
+
import { BrushOverlay } from "./BrushOverlay.js";
|
|
15
|
+
import { useBrush } from "./use-brush.js";
|
|
16
|
+
import type {
|
|
17
|
+
BrushingResult, BrushState, HoverInfo, PickingResult, PluotProps, RawBrushingResult, RawPickingResult,
|
|
18
|
+
RenderParams, TooltipContent,
|
|
19
|
+
} from "./types.js";
|
|
14
20
|
|
|
15
21
|
// Needed due to "SyntaxError: Named export 'decompressFromUint8Array' not found.
|
|
16
22
|
// The requested module 'lz-string' is a CommonJS module,
|
|
@@ -32,28 +38,44 @@ const DEFAULT_3D_VIEW = new Float32Array([
|
|
|
32
38
|
0, 0, -10, 1,
|
|
33
39
|
]);
|
|
34
40
|
|
|
35
|
-
const identity = (param) => param;
|
|
41
|
+
const identity = <T,>(param: T): T => param;
|
|
36
42
|
const noop = () => { };
|
|
37
43
|
|
|
38
44
|
// Mouse movement (in pixels) beyond which a mousedown-to-click is
|
|
39
45
|
// considered a drag rather than a click, so that picking is skipped.
|
|
40
46
|
const DRAG_THRESHOLD_PX = 3;
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
// `pick_wasm` is typed `any` by wasm-bindgen, so `RawPickingResult` is what
|
|
49
|
+
// documents its wire format (see types.ts).
|
|
50
|
+
function normalizePickingResult(data: RawPickingResult): PickingResult {
|
|
51
|
+
return {
|
|
52
|
+
...data,
|
|
53
|
+
layer_results: data.layer_results.map(({ layer_id, info }) => ({
|
|
54
|
+
layer_id,
|
|
47
55
|
// This is needed because serde-wasm-bindgen
|
|
48
56
|
// converts Rust HashMap to JS Map.
|
|
49
|
-
info: Object.fromEntries(
|
|
50
|
-
}))
|
|
51
|
-
}
|
|
52
|
-
return result;
|
|
57
|
+
info: Object.fromEntries(info),
|
|
58
|
+
})),
|
|
59
|
+
};
|
|
53
60
|
}
|
|
54
61
|
|
|
62
|
+
// `brush_wasm` is typed `any` by wasm-bindgen, so `RawBrushingResult` is what
|
|
63
|
+
// documents its wire format (see types.ts).
|
|
64
|
+
function normalizeBrushingResult(data: RawBrushingResult): BrushingResult {
|
|
65
|
+
return {
|
|
66
|
+
...data,
|
|
67
|
+
layer_results: data.layer_results.map(({ layer_id, info, element_info }) => ({
|
|
68
|
+
layer_id,
|
|
69
|
+
// This is needed because serde-wasm-bindgen
|
|
70
|
+
// converts Rust HashMap to JS Map.
|
|
71
|
+
info: Object.fromEntries(info),
|
|
72
|
+
element_info: Object.fromEntries(element_info),
|
|
73
|
+
})),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
55
76
|
|
|
56
|
-
|
|
77
|
+
|
|
78
|
+
export function Pluot(props: PluotProps) {
|
|
57
79
|
const {
|
|
58
80
|
schemaVersion = null,
|
|
59
81
|
width: widthProp,
|
|
@@ -84,15 +106,38 @@ export function Pluot(props) {
|
|
|
84
106
|
enableTooltip = false,
|
|
85
107
|
onClick: onClickProp = null,
|
|
86
108
|
onHover: onHoverProp = null,
|
|
109
|
+
brushUnitsModeX = "Data",
|
|
110
|
+
brushUnitsModeY = "Data",
|
|
111
|
+
brushMarginTop,
|
|
112
|
+
brushMarginRight,
|
|
113
|
+
brushMarginBottom,
|
|
114
|
+
brushMarginLeft,
|
|
115
|
+
enableBrushCreate = false,
|
|
116
|
+
enableBrushEdit = false,
|
|
117
|
+
enableBrushClear = false,
|
|
118
|
+
brushDelay = 1500,
|
|
119
|
+
maybeBrushDelay = 250,
|
|
120
|
+
persistBrush = false,
|
|
121
|
+
brushMode = "Rect",
|
|
122
|
+
brushColor = "#3b6ea5",
|
|
123
|
+
// An omitted `brush` means uncontrolled; a controlled parent signals the
|
|
124
|
+
// empty state with `NO_BRUSH`, never `undefined`.
|
|
125
|
+
brush = null,
|
|
126
|
+
onBrush,
|
|
127
|
+
onBrushEnd,
|
|
128
|
+
onBrushClear,
|
|
129
|
+
|
|
130
|
+
// Temporary workaround. See comments in LruStore.clearCache.
|
|
131
|
+
shouldClearCache = true,
|
|
87
132
|
} = props;
|
|
88
133
|
|
|
89
|
-
const onClick = typeof onClickProp === 'function' ? onClickProp :
|
|
90
|
-
const onHover = typeof onHoverProp === 'function' ? onHoverProp : identity;
|
|
134
|
+
const onClick: (result: PickingResult) => void = typeof onClickProp === 'function' ? onClickProp : noop;
|
|
135
|
+
const onHover: (result: PickingResult) => TooltipContent = typeof onHoverProp === 'function' ? onHoverProp : identity;
|
|
91
136
|
|
|
92
137
|
|
|
93
138
|
|
|
94
139
|
// If cameraMatrix is not provided, then we manage the camera matrix internally.
|
|
95
|
-
const [uncontrolledCameraMatrix, setUncontrolledCameraMatrix] = useState(
|
|
140
|
+
const [uncontrolledCameraMatrix, setUncontrolledCameraMatrix] = useState<CameraMatrix>(
|
|
96
141
|
// Note: We use an initializer function here to avoid
|
|
97
142
|
// sharing the same Float32Array among multiple Pluot
|
|
98
143
|
// component instances that may be rendered on the same page.
|
|
@@ -115,7 +160,7 @@ export function Pluot(props) {
|
|
|
115
160
|
const cameraMatrix = isControlledCamera && controlledCameraMatrix !== null
|
|
116
161
|
? controlledCameraMatrix
|
|
117
162
|
: uncontrolledCameraMatrix;
|
|
118
|
-
const setCameraMatrix = isControlledCamera
|
|
163
|
+
const setCameraMatrix: (nextCameraMatrix: CameraMatrix) => void = isControlledCamera
|
|
119
164
|
? setControlledCameraMatrix
|
|
120
165
|
: setUncontrolledCameraMatrix;
|
|
121
166
|
|
|
@@ -136,11 +181,14 @@ export function Pluot(props) {
|
|
|
136
181
|
|
|
137
182
|
const [supportsWebGpu, supportsWebGpuMessage] = useMemo(checkWebGpuFeatureDetection, []);
|
|
138
183
|
|
|
139
|
-
const svgRef = useRef(null);
|
|
140
|
-
const canvasRef = useRef(null);
|
|
141
|
-
const cameraElementRef = useRef(null);
|
|
184
|
+
const svgRef = useRef<SVGSVGElement | null>(null);
|
|
185
|
+
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
186
|
+
const cameraElementRef = useRef<HTMLDivElement | null>(null);
|
|
187
|
+
// The outer (width x height) element, which is the coordinate space that both
|
|
188
|
+
// the brush overlay and the hover tooltip are positioned within.
|
|
189
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
142
190
|
|
|
143
|
-
const tempButtonRef = useRef(null);
|
|
191
|
+
const tempButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
144
192
|
|
|
145
193
|
// We may want to update these things without triggering a re-render.
|
|
146
194
|
const isRenderingRef = useRef(false);
|
|
@@ -148,31 +196,101 @@ export function Pluot(props) {
|
|
|
148
196
|
|
|
149
197
|
// Used to distinguish a plain click from a click that ends a drag
|
|
150
198
|
// (e.g. panning), so that dragging does not trigger picking.
|
|
151
|
-
const dragStartRef = useRef(null);
|
|
199
|
+
const dragStartRef = useRef<{ x: number, y: number } | null>(null);
|
|
152
200
|
const didDragRef = useRef(false);
|
|
153
201
|
|
|
154
202
|
// TODO: do we want to use the backlog approach or not?
|
|
155
203
|
// (Similar to the one used in the Vitessce heatmap)
|
|
156
204
|
// Reference: https://github.com/vitessce/vitessce/blob/71f17fb605768e0428fb15ed87b3ea34bcbb4803/packages/view-types/heatmap/src/Heatmap.js#L368
|
|
157
205
|
//const backlogRef = useRef([]);
|
|
158
|
-
const [backlogIteration, incBacklogIteration] = useReducer(i => i + 1, 0);
|
|
206
|
+
const [backlogIteration, incBacklogIteration] = useReducer((i: number) => i + 1, 0);
|
|
159
207
|
|
|
160
208
|
const [isWasmReady, setIsWasmReady] = useState(false);
|
|
161
209
|
const [didFirstRender, setDidFirstRender] = useState(false);
|
|
162
210
|
const [bailedEarly, setBailedEarly] = useState(true);
|
|
163
211
|
|
|
164
|
-
const [pickingResult, setPickingResult] = useState(null);
|
|
165
212
|
// hoverInfo.mouseX/mouseY are in the coordinate space of the outer
|
|
166
213
|
// (width x height) container, used to position the hover tooltip.
|
|
167
|
-
const [hoverInfo, setHoverInfo] = useState(null);
|
|
214
|
+
const [hoverInfo, setHoverInfo] = useState<HoverInfo | null>(null);
|
|
168
215
|
|
|
169
216
|
const progressBarId = useId();
|
|
170
217
|
|
|
218
|
+
// Runs the brush query against the wasm module for a given brush state,
|
|
219
|
+
// analogous to `pick` below (defined here, ahead of `pick`, since `useBrush`
|
|
220
|
+
// needs it immediately).
|
|
221
|
+
const runBrush = useEffectEvent(async (state: BrushState): Promise<BrushingResult|undefined> => {
|
|
222
|
+
|
|
223
|
+
if (!isWasmReady) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const renderParams: RenderParams = {
|
|
228
|
+
schema_version: schemaVersion,
|
|
229
|
+
width,
|
|
230
|
+
height,
|
|
231
|
+
format: format,
|
|
232
|
+
margin_bottom: marginBottom,
|
|
233
|
+
margin_left: marginLeft,
|
|
234
|
+
margin_top: marginTop,
|
|
235
|
+
margin_right: marginRight,
|
|
236
|
+
device_pixel_ratio: window.devicePixelRatio,
|
|
237
|
+
aspect_ratio_mode: aspectRatioMode,
|
|
238
|
+
aspect_ratio_alignment_mode: aspectRatioAlignmentMode,
|
|
239
|
+
view_mode: viewMode,
|
|
240
|
+
pickable: false,
|
|
241
|
+
camera_view: cameraMatrix,
|
|
242
|
+
plot_id: plotId,
|
|
243
|
+
plot_type: plotType,
|
|
244
|
+
stores,
|
|
245
|
+
plot_params: plotParams,
|
|
246
|
+
timeout: currentTimeout.current,
|
|
247
|
+
wait_for_store_gets: false,
|
|
248
|
+
cache_enabled: true,
|
|
249
|
+
svg_compression_enabled: true,
|
|
250
|
+
svg_include_document: false,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// Brush vertices are container-relative pixels with Y increasing downwards;
|
|
254
|
+
// the wasm side expects screen coordinates with Y increasing upwards (as
|
|
255
|
+
// with the `screenCoordX`/`screenCoordY` passed to `pick_wasm` below).
|
|
256
|
+
const brushParams = {
|
|
257
|
+
screen_vertices: state.vertices.map((vertex) => ({ x: vertex.x_pixels, y: height - vertex.y_pixels })),
|
|
258
|
+
brush_units_mode_x: brushUnitsModeX,
|
|
259
|
+
brush_units_mode_y: brushUnitsModeY,
|
|
260
|
+
brush_mode: state.shape,
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
return normalizeBrushingResult(await brush_wasm(renderParams, brushParams));
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const {
|
|
267
|
+
brushState,
|
|
268
|
+
overlayRef: brushOverlayRef,
|
|
269
|
+
geometry: brushGeometry,
|
|
270
|
+
pressProgress,
|
|
271
|
+
isBrushHovered,
|
|
272
|
+
isBrushingRef,
|
|
273
|
+
shouldSuppressClickRef,
|
|
274
|
+
onVertexMouseDown,
|
|
275
|
+
onEdgeMouseDown,
|
|
276
|
+
onClearClick,
|
|
277
|
+
} = useBrush({
|
|
278
|
+
containerRef,
|
|
279
|
+
width, height,
|
|
280
|
+
marginTop, marginRight, marginBottom, marginLeft,
|
|
281
|
+
aspectRatioMode, aspectRatioAlignmentMode, cameraMatrix,
|
|
282
|
+
brushUnitsModeX, brushUnitsModeY,
|
|
283
|
+
brushMarginTop, brushMarginRight, brushMarginBottom, brushMarginLeft,
|
|
284
|
+
enableBrushCreate, enableBrushEdit, enableBrushClear,
|
|
285
|
+
brushDelay, maybeBrushDelay, persistBrush, brushMode,
|
|
286
|
+
brush, onBrush, onBrushEnd, onBrushClear, runBrush,
|
|
287
|
+
});
|
|
288
|
+
|
|
171
289
|
useLayoutEffect(() => {
|
|
172
290
|
initialize().then(() => setIsWasmReady(getIsWasmReady()));
|
|
173
291
|
}, []);
|
|
174
292
|
|
|
175
|
-
const wheelHandler = useEffectEvent((event) => {
|
|
293
|
+
const wheelHandler = useEffectEvent((event: WheelEvent) => {
|
|
176
294
|
const onWheel = viewMode === "3d" ? onWheel3d : onWheel2d;
|
|
177
295
|
const nextCameraMatrix = onWheel({
|
|
178
296
|
width,
|
|
@@ -189,7 +307,11 @@ export function Pluot(props) {
|
|
|
189
307
|
setCameraMatrix(nextCameraMatrix);
|
|
190
308
|
});
|
|
191
309
|
|
|
192
|
-
const mouseMoveHandler = useEffectEvent((event) => {
|
|
310
|
+
const mouseMoveHandler = useEffectEvent((event: MouseEvent) => {
|
|
311
|
+
// A drag that is drawing or editing a brush must not also pan/rotate the camera.
|
|
312
|
+
if (isBrushingRef.current) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
193
315
|
const onMouseMove = viewMode === "3d" ? onMouseMove3d : onMouseMove2d;
|
|
194
316
|
const nextCameraMatrix = onMouseMove({
|
|
195
317
|
width,
|
|
@@ -208,8 +330,8 @@ export function Pluot(props) {
|
|
|
208
330
|
|
|
209
331
|
// Runs the picking query against the wasm module and returns the normalized result.
|
|
210
332
|
// Shared by the click (pickFrame) and hover (hoverFrame) callbacks below.
|
|
211
|
-
const pick = useEffectEvent(async (screenCoordX, screenCoordY) => {
|
|
212
|
-
const renderParams = {
|
|
333
|
+
const pick = useEffectEvent(async (screenCoordX: number, screenCoordY: number): Promise<PickingResult> => {
|
|
334
|
+
const renderParams: RenderParams = {
|
|
213
335
|
schema_version: schemaVersion,
|
|
214
336
|
width,
|
|
215
337
|
height,
|
|
@@ -251,12 +373,12 @@ export function Pluot(props) {
|
|
|
251
373
|
});
|
|
252
374
|
|
|
253
375
|
// The click-picking callback.
|
|
254
|
-
const pickFrame = useEffectEvent(async (screenCoordX, screenCoordY) => {
|
|
255
|
-
|
|
376
|
+
const pickFrame = useEffectEvent(async (screenCoordX: number, screenCoordY: number) => {
|
|
377
|
+
onClick(await pick(screenCoordX, screenCoordY));
|
|
256
378
|
});
|
|
257
379
|
|
|
258
380
|
// The hover-picking callback.
|
|
259
|
-
const hoverFrame = useEffectEvent(async (screenCoordX, screenCoordY) => {
|
|
381
|
+
const hoverFrame = useEffectEvent(async (screenCoordX: number, screenCoordY: number) => {
|
|
260
382
|
const result = await pick(screenCoordX, screenCoordY);
|
|
261
383
|
setHoverInfo({
|
|
262
384
|
content: onHover(result),
|
|
@@ -292,11 +414,15 @@ export function Pluot(props) {
|
|
|
292
414
|
|
|
293
415
|
// Track mousedown -> mousemove distance so that a drag (e.g. panning)
|
|
294
416
|
// that ends on the camera element does not also trigger a click/pick.
|
|
295
|
-
const mouseDownHandler = (event) => {
|
|
417
|
+
const mouseDownHandler = (event: MouseEvent) => {
|
|
296
418
|
dragStartRef.current = { x: event.clientX, y: event.clientY };
|
|
297
419
|
didDragRef.current = false;
|
|
420
|
+
// A brush drag that ended outside the camera element never produced the
|
|
421
|
+
// click that would have consumed this flag, so clear it as the next
|
|
422
|
+
// interaction begins rather than letting it suppress that one too.
|
|
423
|
+
shouldSuppressClickRef.current = false;
|
|
298
424
|
};
|
|
299
|
-
const dragDetectHandler = (event) => {
|
|
425
|
+
const dragDetectHandler = (event: MouseEvent) => {
|
|
300
426
|
if (!dragStartRef.current) {
|
|
301
427
|
return;
|
|
302
428
|
}
|
|
@@ -310,19 +436,23 @@ export function Pluot(props) {
|
|
|
310
436
|
cameraEl.addEventListener("mousemove", dragDetectHandler);
|
|
311
437
|
|
|
312
438
|
// Set up an onClick handler for picking.
|
|
313
|
-
const clickHandler = (event) => {
|
|
439
|
+
const clickHandler = (event: MouseEvent) => {
|
|
314
440
|
const wasDrag = didDragRef.current;
|
|
441
|
+
// A brush drag (or a click on the clear button) ends with a click on the
|
|
442
|
+
// camera element, which should not also run a picking query.
|
|
443
|
+
const wasBrush = shouldSuppressClickRef.current;
|
|
315
444
|
dragStartRef.current = null;
|
|
316
445
|
didDragRef.current = false;
|
|
317
|
-
|
|
446
|
+
shouldSuppressClickRef.current = false;
|
|
447
|
+
if (enableClick && !wasDrag && !wasBrush) {
|
|
318
448
|
pickFrame(event.offsetX, event.offsetY);
|
|
319
449
|
}
|
|
320
450
|
};
|
|
321
451
|
cameraEl.addEventListener("click", clickHandler);
|
|
322
452
|
|
|
323
453
|
// Set up hover handlers for picking, only when the onHover prop is provided.
|
|
324
|
-
const hoverMoveHandler = (event) => {
|
|
325
|
-
if (enableTooltip) {
|
|
454
|
+
const hoverMoveHandler = (event: MouseEvent) => {
|
|
455
|
+
if (enableTooltip && !isBrushingRef.current) {
|
|
326
456
|
throttledHoverFrame(event.offsetX, event.offsetY);
|
|
327
457
|
}
|
|
328
458
|
};
|
|
@@ -354,7 +484,7 @@ export function Pluot(props) {
|
|
|
354
484
|
isRenderingRef.current = true;
|
|
355
485
|
console.log('wasm.render');
|
|
356
486
|
|
|
357
|
-
const renderParams = {
|
|
487
|
+
const renderParams: RenderParams = {
|
|
358
488
|
schema_version: schemaVersion,
|
|
359
489
|
width,
|
|
360
490
|
height,
|
|
@@ -383,7 +513,7 @@ export function Pluot(props) {
|
|
|
383
513
|
};
|
|
384
514
|
|
|
385
515
|
// Wrap render_wasm in try/catch, to handle Rust panics.
|
|
386
|
-
let arr;
|
|
516
|
+
let arr: Uint8Array;
|
|
387
517
|
try {
|
|
388
518
|
arr = await render_wasm(renderParams);
|
|
389
519
|
|
|
@@ -436,9 +566,9 @@ export function Pluot(props) {
|
|
|
436
566
|
setBailedEarly(false); // Update this to hide the loading indicator.
|
|
437
567
|
|
|
438
568
|
// Clear the LRU cache for the store (via its store_name) corresponding to the rendered plot.
|
|
439
|
-
Object.keys(stores).forEach(storeName => {
|
|
569
|
+
Object.keys(stores ?? {}).forEach(storeName => {
|
|
440
570
|
const storeUsed = getStore(storeName);
|
|
441
|
-
if (storeUsed &&
|
|
571
|
+
if (storeUsed && typeof storeUsed.clearCache === 'function' && shouldClearCache) {
|
|
442
572
|
storeUsed.clearCache();
|
|
443
573
|
}
|
|
444
574
|
});
|
|
@@ -496,7 +626,7 @@ export function Pluot(props) {
|
|
|
496
626
|
|
|
497
627
|
// Position the hover tooltip so that it grows diagonally away from whichever
|
|
498
628
|
// quadrant of the plot the mouse currently occupies, to avoid clipping.
|
|
499
|
-
const hoverStyle = useMemo(() => {
|
|
629
|
+
const hoverStyle = useMemo<CSSProperties | null>(() => {
|
|
500
630
|
if (!hoverInfo) {
|
|
501
631
|
return null;
|
|
502
632
|
}
|
|
@@ -509,6 +639,8 @@ export function Pluot(props) {
|
|
|
509
639
|
return {
|
|
510
640
|
position: "absolute",
|
|
511
641
|
pointerEvents: "none",
|
|
642
|
+
// Above the brush overlay, so a persisted brush does not tint the tooltip.
|
|
643
|
+
zIndex: 2,
|
|
512
644
|
...(isTop ? { top: mouseY + offsetPx } : { bottom: height - mouseY + offsetPx + extraPx }),
|
|
513
645
|
...(isLeft ? { left: mouseX + offsetPx + extraPx } : { right: width - mouseX + offsetPx }),
|
|
514
646
|
};
|
|
@@ -516,7 +648,14 @@ export function Pluot(props) {
|
|
|
516
648
|
|
|
517
649
|
return (
|
|
518
650
|
<>
|
|
519
|
-
<div
|
|
651
|
+
<div
|
|
652
|
+
ref={containerRef}
|
|
653
|
+
style={{
|
|
654
|
+
width, height, position: "relative", backgroundColor,
|
|
655
|
+
// Long-clicking to start a brush otherwise selects surrounding text.
|
|
656
|
+
userSelect: enableBrushCreate ? "none" : undefined,
|
|
657
|
+
}}
|
|
658
|
+
>
|
|
520
659
|
{!supportsWebGpu ? (
|
|
521
660
|
<p>{supportsWebGpuMessage}</p>
|
|
522
661
|
) : null}
|
|
@@ -570,8 +709,22 @@ export function Pluot(props) {
|
|
|
570
709
|
|
|
571
710
|
/>
|
|
572
711
|
)}
|
|
712
|
+
<BrushOverlay
|
|
713
|
+
width={width}
|
|
714
|
+
height={height}
|
|
715
|
+
overlayRef={brushOverlayRef}
|
|
716
|
+
geometry={brushGeometry}
|
|
717
|
+
color={brushColor}
|
|
718
|
+
brushState={brushState}
|
|
719
|
+
pressProgress={pressProgress}
|
|
720
|
+
isBrushHovered={isBrushHovered}
|
|
721
|
+
enableBrushEdit={enableBrushEdit}
|
|
722
|
+
onVertexMouseDown={onVertexMouseDown}
|
|
723
|
+
onEdgeMouseDown={onEdgeMouseDown}
|
|
724
|
+
onClearClick={onClearClick}
|
|
725
|
+
/>
|
|
573
726
|
{hoverInfo ? (
|
|
574
|
-
<div style={hoverStyle}>
|
|
727
|
+
<div style={hoverStyle ?? undefined}>
|
|
575
728
|
<Tooltip content={hoverInfo.content} asTable />
|
|
576
729
|
</div>
|
|
577
730
|
) : null}
|
|
@@ -1,9 +1,25 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
import type { TooltipProps } from "./types.js";
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
const boxShadow = '5px 5px 15px rgb(0 0 0 / 20%)';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
// Table cell values come from arbitrary onHover return objects, so narrow
|
|
8
|
+
// them to something React can actually render.
|
|
9
|
+
function renderCell(value: unknown): ReactNode {
|
|
10
|
+
if (value === null || value === undefined || typeof value === "boolean") {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
if (React.isValidElement(value)) {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
return JSON.stringify(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Tooltip(props: TooltipProps) {
|
|
7
23
|
const {
|
|
8
24
|
content,
|
|
9
25
|
asTable = false,
|
|
@@ -24,7 +40,7 @@ export function Tooltip(props) {
|
|
|
24
40
|
{Object.entries(content).map(([key, value]) => (
|
|
25
41
|
<tr key={key}>
|
|
26
42
|
<th style={{ border: 'none', fontSize: '12px', outline: 0, padding: '0 2px', textAlign: 'left' }}>{key}</th>
|
|
27
|
-
<td style={{ border: 'none', fontSize: '12px', outline: 0, padding: '0 2px', textAlign: 'left' }}>{value}</td>
|
|
43
|
+
<td style={{ border: 'none', fontSize: '12px', outline: 0, padding: '0 2px', textAlign: 'left' }}>{renderCell(value)}</td>
|
|
28
44
|
</tr>
|
|
29
45
|
))}
|
|
30
46
|
</tbody>
|