@pluot/react 0.1.1 → 0.1.3
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 +1283 -914
- package/dist-tsc/Pluot.d.ts.map +1 -1
- package/dist-tsc/Pluot.js +75 -45
- package/package.json +3 -3
- package/src/Pluot.jsx +101 -54
- package/dist-tsc/3d-view-controls.d.ts +0 -17
- package/dist-tsc/3d-view-controls.d.ts.map +0 -1
- package/dist-tsc/3d-view-controls.js +0 -223
- package/dist-tsc/dom-2d-camera.d.ts +0 -26
- package/dist-tsc/dom-2d-camera.d.ts.map +0 -1
- package/dist-tsc/dom-2d-camera.js +0 -303
- package/dist-tsc/feature-detection.d.ts +0 -5
- package/dist-tsc/feature-detection.d.ts.map +0 -1
- package/dist-tsc/feature-detection.js +0 -6
- package/dist-tsc/lru-store.d.ts +0 -4
- package/dist-tsc/lru-store.d.ts.map +0 -1
- package/dist-tsc/lru-store.js +0 -137
package/dist-tsc/Pluot.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Pluot.d.ts","sourceRoot":"","sources":["../src/Pluot.jsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Pluot.d.ts","sourceRoot":"","sources":["../src/Pluot.jsx"],"names":[],"mappings":"AA+CA,uCAsaC"}
|
package/dist-tsc/Pluot.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import React, { useLayoutEffect, useEffect, useEffectEvent, useRef, useState, useMemo, useReducer, useCallback } from "react";
|
|
3
|
-
import { mat4, vec4 } from "gl-matrix";
|
|
2
|
+
import React, { useLayoutEffect, useEffect, useEffectEvent, useRef, useState, useMemo, useReducer, useCallback, useId } from "react";
|
|
4
3
|
import lzs from "lz-string";
|
|
5
4
|
import { isEqual, throttle } from "lodash-es";
|
|
6
|
-
import {
|
|
7
|
-
import { initialize, getIsWasmReady, render_wasm, pick_wasm, setStore, getStore, getBounds, getCameraMatrixFromBounds, checkWebGpuFeatureDetection, onMouseMove2d, onWheel2d, onMouseMove3d, onWheel3d, } from '@pluot/core';
|
|
5
|
+
import { initialize, getIsWasmReady, render_wasm, pick_wasm, normalizeStores, getStore, getBounds, getCameraMatrixFromBounds, checkWebGpuFeatureDetection, onMouseMove2d, onWheel2d, onMouseMove3d, onWheel3d, } from '@pluot/core';
|
|
8
6
|
// Needed due to "SyntaxError: Named export 'decompressFromUint8Array' not found.
|
|
9
7
|
// The requested module 'lz-string' is a CommonJS module,
|
|
10
8
|
// which may not support all module.exports as named exports."
|
|
@@ -34,10 +32,10 @@ function normalizePickingResult(data) {
|
|
|
34
32
|
return result;
|
|
35
33
|
}
|
|
36
34
|
export function Pluot(props) {
|
|
37
|
-
const { width: widthProp, height: heightProp, plotId, plotType, store, storeName: storeNameProp, plotParams, viewMode = "2d", marginBottom = 100.0, marginLeft = 100.0, marginTop = 100.0, marginRight = 100.0, aspectRatioMode = "Contain", // "Ignore", "Contain", "Cover"
|
|
35
|
+
const { width: widthProp, height: heightProp, plotId, plotType, store: storeProp, storeName: storeNameProp, stores: storesProp, plotParams, viewMode = "2d", marginBottom = 100.0, marginLeft = 100.0, marginTop = 100.0, marginRight = 100.0, aspectRatioMode = "Contain", // "Ignore", "Contain", "Cover"
|
|
38
36
|
aspectRatioAlignmentMode = "Start", // "Center", "Start", "End"
|
|
39
37
|
format = "Raster", // "Raster", "Vector"
|
|
40
|
-
minTimeout = 32, maxTimeout =
|
|
38
|
+
minTimeout = 32, maxTimeout = 5000, allowSimultaneousRenders = true, debugMargins = false, cameraMatrix: controlledCameraMatrix = null, setCameraMatrix: setControlledCameraMatrix = null, enablePicking = true, backgroundColor = undefined, } = props;
|
|
41
39
|
// If cameraMatrix is not provided, then we manage the camera matrix internally.
|
|
42
40
|
const [uncontrolledCameraMatrix, setUncontrolledCameraMatrix] = useState(
|
|
43
41
|
// Note: We use an initializer function here to avoid
|
|
@@ -65,19 +63,15 @@ export function Pluot(props) {
|
|
|
65
63
|
const width = Math.floor(widthProp);
|
|
66
64
|
const height = Math.floor(heightProp);
|
|
67
65
|
const isVector = format === "Vector";
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return setStore(store, plotId);
|
|
78
|
-
}
|
|
79
|
-
throw new Error("Either storeName or store must be provided.");
|
|
80
|
-
}, [storeNameProp, store]);
|
|
66
|
+
// Build the top-level `stores` map that RenderParams expects: a mapping from
|
|
67
|
+
// store name to its derived `ZarrStoreInfo` metadata.
|
|
68
|
+
const stores = useMemo(() => normalizeStores({
|
|
69
|
+
stores: storesProp,
|
|
70
|
+
store: storeProp,
|
|
71
|
+
storeName: storeNameProp,
|
|
72
|
+
plotId,
|
|
73
|
+
register: true,
|
|
74
|
+
}), [storeNameProp, storeProp, storesProp, plotId]);
|
|
81
75
|
const [supportsWebGpu, supportsWebGpuMessage] = useMemo(checkWebGpuFeatureDetection, []);
|
|
82
76
|
const svgRef = useRef(null);
|
|
83
77
|
const canvasRef = useRef(null);
|
|
@@ -85,7 +79,7 @@ export function Pluot(props) {
|
|
|
85
79
|
const tempButtonRef = useRef(null);
|
|
86
80
|
// We may want to update these things without triggering a re-render.
|
|
87
81
|
const isRenderingRef = useRef(false);
|
|
88
|
-
const currentTimeout = useRef(
|
|
82
|
+
const currentTimeout = useRef(minTimeout);
|
|
89
83
|
// TODO: do we want to use the backlog approach or not?
|
|
90
84
|
// (Similar to the one used in the Vitessce heatmap)
|
|
91
85
|
// Reference: https://github.com/vitessce/vitessce/blob/71f17fb605768e0428fb15ed87b3ea34bcbb4803/packages/view-types/heatmap/src/Heatmap.js#L368
|
|
@@ -95,6 +89,7 @@ export function Pluot(props) {
|
|
|
95
89
|
const [didFirstRender, setDidFirstRender] = useState(false);
|
|
96
90
|
const [bailedEarly, setBailedEarly] = useState(true);
|
|
97
91
|
const [pickingResult, setPickingResult] = useState(null);
|
|
92
|
+
const progressBarId = useId();
|
|
98
93
|
useLayoutEffect(() => {
|
|
99
94
|
initialize().then(() => setIsWasmReady(getIsWasmReady()));
|
|
100
95
|
}, []);
|
|
@@ -130,21 +125,6 @@ export function Pluot(props) {
|
|
|
130
125
|
}, cameraMatrix, event);
|
|
131
126
|
setCameraMatrix(nextCameraMatrix);
|
|
132
127
|
});
|
|
133
|
-
// Set up FUNCTIONAL camera.
|
|
134
|
-
useEffect(() => {
|
|
135
|
-
// Set up the camera.
|
|
136
|
-
const cameraEl = cameraElementRef.current;
|
|
137
|
-
if (!cameraEl) {
|
|
138
|
-
return () => { };
|
|
139
|
-
}
|
|
140
|
-
// Create a 2D camera for handling zoom and pan.
|
|
141
|
-
cameraEl.addEventListener("mousemove", mouseMoveHandler);
|
|
142
|
-
cameraEl.addEventListener("wheel", wheelHandler);
|
|
143
|
-
return () => {
|
|
144
|
-
cameraEl.removeEventListener("mousemove", mouseMoveHandler);
|
|
145
|
-
cameraEl.removeEventListener("wheel", wheelHandler);
|
|
146
|
-
};
|
|
147
|
-
}, [viewMode]);
|
|
148
128
|
// The picking callback.
|
|
149
129
|
const pickFrame = useEffectEvent(async (screenCoordX, screenCoordY) => {
|
|
150
130
|
const renderParams = {
|
|
@@ -164,7 +144,7 @@ export function Pluot(props) {
|
|
|
164
144
|
camera_view: cameraMatrix,
|
|
165
145
|
plot_id: plotId,
|
|
166
146
|
plot_type: plotType,
|
|
167
|
-
|
|
147
|
+
stores,
|
|
168
148
|
plot_params: plotParams,
|
|
169
149
|
// Reduce the timeout value to improve responsiveness during data loading (bailed-early renders)?
|
|
170
150
|
timeout: currentTimeout.current, // in ms // Note: will not have any effect when wait_for_store_gets is false.
|
|
@@ -174,11 +154,34 @@ export function Pluot(props) {
|
|
|
174
154
|
svg_include_document: false,
|
|
175
155
|
};
|
|
176
156
|
const layerHeight = height - marginTop - marginBottom;
|
|
157
|
+
// TODO: wrap pick_wasm in a try/catch
|
|
177
158
|
setPickingResult(normalizePickingResult(await pick_wasm(renderParams,
|
|
178
159
|
// The coordinates are relative to the "layer" (the camera region), not the full width/height.
|
|
179
160
|
// We also need to flip the Y coordinate so that positive is up.
|
|
180
161
|
screenCoordX + marginLeft, marginBottom + (layerHeight - screenCoordY))));
|
|
181
162
|
});
|
|
163
|
+
// Set up the camera and picking handlers.
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
const cameraEl = cameraElementRef.current;
|
|
166
|
+
if (!cameraEl) {
|
|
167
|
+
return () => { };
|
|
168
|
+
}
|
|
169
|
+
// Create a 2D camera for handling zoom and pan.
|
|
170
|
+
cameraEl.addEventListener("mousemove", mouseMoveHandler);
|
|
171
|
+
cameraEl.addEventListener("wheel", wheelHandler);
|
|
172
|
+
// Set up an onClick handler for picking.
|
|
173
|
+
const clickHandler = (event) => {
|
|
174
|
+
if (enablePicking) {
|
|
175
|
+
pickFrame(event.offsetX, event.offsetY);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
cameraEl.addEventListener("click", clickHandler);
|
|
179
|
+
return () => {
|
|
180
|
+
cameraEl.removeEventListener("mousemove", mouseMoveHandler);
|
|
181
|
+
cameraEl.removeEventListener("wheel", wheelHandler);
|
|
182
|
+
cameraEl.removeEventListener("click", clickHandler);
|
|
183
|
+
};
|
|
184
|
+
}, [viewMode, enablePicking]);
|
|
182
185
|
// The renderFrame callback.
|
|
183
186
|
// We use useEffectEvent because we want to "see"
|
|
184
187
|
// the latest values of viewMatrix, plotProps, etc.
|
|
@@ -202,7 +205,7 @@ export function Pluot(props) {
|
|
|
202
205
|
camera_view: cameraMatrix,
|
|
203
206
|
plot_id: plotId,
|
|
204
207
|
plot_type: plotType,
|
|
205
|
-
|
|
208
|
+
stores,
|
|
206
209
|
plot_params: plotParams,
|
|
207
210
|
// Reduce the timeout value to improve responsiveness during data loading (bailed-early renders)?
|
|
208
211
|
timeout: currentTimeout.current, // in ms // Note: will not have any effect when wait_for_store_gets is false.
|
|
@@ -249,18 +252,25 @@ export function Pluot(props) {
|
|
|
249
252
|
ctx.putImageData(imageData, 0, 0);
|
|
250
253
|
const frameBailedEarly = arr.at(-1) === 1;
|
|
251
254
|
if (frameBailedEarly) {
|
|
252
|
-
|
|
255
|
+
// We multiply the current timeout by two to implement an exponential backoff
|
|
256
|
+
// while the Rust side is bailing early.
|
|
257
|
+
// A downstream useEffect restarts the exponential backoff from scratch
|
|
258
|
+
// if any other plotting parameters change.
|
|
259
|
+
currentTimeout.current = Math.min(currentTimeout.current * 2, maxTimeout);
|
|
253
260
|
incBacklogIteration(); // Increment this to force a re-render.
|
|
254
261
|
setBailedEarly(true); // Update this to show the loading indicator.
|
|
255
262
|
}
|
|
256
263
|
else {
|
|
257
264
|
// Successful render.
|
|
265
|
+
currentTimeout.current = minTimeout;
|
|
258
266
|
setBailedEarly(false); // Update this to hide the loading indicator.
|
|
259
267
|
// Clear the LRU cache for the store (via its store_name) corresponding to the rendered plot.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
storeUsed.clearCache
|
|
263
|
-
|
|
268
|
+
Object.keys(stores).forEach(storeName => {
|
|
269
|
+
const storeUsed = getStore(storeName);
|
|
270
|
+
if (storeUsed && storeUsed.clearCache && typeof storeUsed.clearCache === 'function') {
|
|
271
|
+
storeUsed.clearCache();
|
|
272
|
+
}
|
|
273
|
+
});
|
|
264
274
|
}
|
|
265
275
|
}
|
|
266
276
|
setDidFirstRender(true);
|
|
@@ -274,6 +284,15 @@ export function Pluot(props) {
|
|
|
274
284
|
useEffect(() => {
|
|
275
285
|
return () => throttledRender.cancel();
|
|
276
286
|
}, [throttledRender]);
|
|
287
|
+
// Reset the backoff timeout whenever plot parameters change so the next
|
|
288
|
+
// sequence of bailed-early renders starts from the minimum again.
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
currentTimeout.current = minTimeout;
|
|
291
|
+
}, [plotId, plotType, plotParams, stores, format,
|
|
292
|
+
width, height, aspectRatioMode, aspectRatioAlignmentMode,
|
|
293
|
+
marginLeft, marginRight, marginTop, marginBottom,
|
|
294
|
+
cameraMatrix,
|
|
295
|
+
]);
|
|
277
296
|
// TODO: use react-query?
|
|
278
297
|
useEffect(() => {
|
|
279
298
|
if (!isWasmReady) {
|
|
@@ -289,14 +308,25 @@ export function Pluot(props) {
|
|
|
289
308
|
}
|
|
290
309
|
// Render on the next animation frame.
|
|
291
310
|
throttledRender();
|
|
292
|
-
}, [isWasmReady, didFirstRender, cameraMatrix, backlogIteration, plotId, plotType, plotParams,
|
|
311
|
+
}, [isWasmReady, didFirstRender, cameraMatrix, backlogIteration, plotId, plotType, plotParams, stores, format,
|
|
293
312
|
width, height, aspectRatioMode, aspectRatioAlignmentMode, marginLeft, marginRight, marginTop, marginBottom]);
|
|
294
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", { style: { width, height, position: "relative" }, children: [!supportsWebGpu ? (_jsx("p", { children: supportsWebGpuMessage })) : null, _jsx("div", { ref: cameraElementRef, style: {
|
|
313
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { style: { width, height, position: "relative", backgroundColor }, children: [!supportsWebGpu ? (_jsx("p", { children: supportsWebGpuMessage })) : null, _jsx("div", { ref: cameraElementRef, style: {
|
|
295
314
|
position: "absolute",
|
|
296
315
|
top: marginTop,
|
|
297
316
|
left: marginLeft,
|
|
298
317
|
width: width - marginLeft - marginRight,
|
|
299
318
|
height: height - marginTop - marginBottom,
|
|
300
319
|
border: `${debugMargins ? 1 : 0}px solid red`,
|
|
301
|
-
} }),
|
|
320
|
+
} }), bailedEarly ? (_jsx("progress", { id: progressBarId, "aria-label": "Loading...", style: {
|
|
321
|
+
bottom: 0,
|
|
322
|
+
left: 0,
|
|
323
|
+
width: '100%',
|
|
324
|
+
position: 'absolute'
|
|
325
|
+
} })) : null, isVector ? (_jsx("svg", { ref: svgRef, style: { width, height, border: `${debugMargins ? 1 : 0}px solid black` }, width: width, height: height, viewBox: `0 0 ${width} ${height}`, xmlns: "http://www.w3.org/2000/svg", ...(bailedEarly ? ({
|
|
326
|
+
['aria-busy']: true,
|
|
327
|
+
['aria-describedby']: progressBarId,
|
|
328
|
+
}) : {}) })) : (_jsx("canvas", { ref: canvasRef, style: { width, height, border: `${debugMargins ? 1 : 0}px solid black` }, width: width, height: height, ...(bailedEarly ? ({
|
|
329
|
+
['aria-busy']: true,
|
|
330
|
+
['aria-describedby']: progressBarId,
|
|
331
|
+
}) : {}) }))] }), _jsx("button", { ref: tempButtonRef, style: { display: 'none' }, children: "Try lookAt" }), pickingResult ? (_jsx("pre", { children: JSON.stringify(pickingResult, null, 2) })) : null] }));
|
|
302
332
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pluot/react",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.3",
|
|
5
5
|
"description": "React component for visualization with Pluot",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"author": "Mark Keller",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"lightua": "0.1.0",
|
|
22
|
-
"zarrita": "^0.
|
|
22
|
+
"zarrita": "^0.7.3",
|
|
23
23
|
"quick-lru": "^7.0.1",
|
|
24
24
|
"d3-zoom": "^3.0.0",
|
|
25
25
|
"d3-selection": "^3.0.0",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"mouse-wheel": "^1.0.2",
|
|
37
37
|
"lz-string": "^1.5.0",
|
|
38
38
|
"lodash-es": "^4.17.23",
|
|
39
|
-
"@pluot/core": "0.1.
|
|
39
|
+
"@pluot/core": "0.1.3"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"vitest": "^3.2.4",
|
package/src/Pluot.jsx
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import React, { useLayoutEffect, useEffect, useEffectEvent, useRef, useState, useMemo, useReducer, useCallback } from "react";
|
|
2
|
-
import { mat4, vec4 } from "gl-matrix";
|
|
1
|
+
import React, { useLayoutEffect, useEffect, useEffectEvent, useRef, useState, useMemo, useReducer, useCallback, useId } from "react";
|
|
3
2
|
import lzs from "lz-string";
|
|
4
3
|
import { isEqual, throttle } from "lodash-es";
|
|
5
|
-
import { FetchStore } from 'zarrita';
|
|
6
4
|
import {
|
|
7
5
|
initialize, getIsWasmReady,
|
|
8
6
|
render_wasm, pick_wasm,
|
|
9
|
-
|
|
7
|
+
normalizeStores, getStore,
|
|
10
8
|
getBounds, getCameraMatrixFromBounds,
|
|
11
9
|
checkWebGpuFeatureDetection,
|
|
12
10
|
onMouseMove2d, onWheel2d,
|
|
@@ -53,8 +51,9 @@ export function Pluot(props) {
|
|
|
53
51
|
height: heightProp,
|
|
54
52
|
plotId,
|
|
55
53
|
plotType,
|
|
56
|
-
store,
|
|
54
|
+
store: storeProp,
|
|
57
55
|
storeName: storeNameProp,
|
|
56
|
+
stores: storesProp,
|
|
58
57
|
plotParams,
|
|
59
58
|
viewMode = "2d",
|
|
60
59
|
marginBottom = 100.0,
|
|
@@ -65,11 +64,13 @@ export function Pluot(props) {
|
|
|
65
64
|
aspectRatioAlignmentMode = "Start", // "Center", "Start", "End"
|
|
66
65
|
format = "Raster", // "Raster", "Vector"
|
|
67
66
|
minTimeout = 32,
|
|
68
|
-
maxTimeout =
|
|
67
|
+
maxTimeout = 5000,
|
|
69
68
|
allowSimultaneousRenders = true,
|
|
70
69
|
debugMargins = false,
|
|
71
70
|
cameraMatrix: controlledCameraMatrix = null,
|
|
72
71
|
setCameraMatrix: setControlledCameraMatrix = null,
|
|
72
|
+
enablePicking = true,
|
|
73
|
+
backgroundColor = undefined,
|
|
73
74
|
} = props;
|
|
74
75
|
|
|
75
76
|
// If cameraMatrix is not provided, then we manage the camera matrix internally.
|
|
@@ -105,19 +106,15 @@ export function Pluot(props) {
|
|
|
105
106
|
|
|
106
107
|
const isVector = format === "Vector";
|
|
107
108
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return setStore(store, plotId);
|
|
118
|
-
}
|
|
119
|
-
throw new Error("Either storeName or store must be provided.");
|
|
120
|
-
}, [storeNameProp, store]);
|
|
109
|
+
// Build the top-level `stores` map that RenderParams expects: a mapping from
|
|
110
|
+
// store name to its derived `ZarrStoreInfo` metadata.
|
|
111
|
+
const stores = useMemo(() => normalizeStores({
|
|
112
|
+
stores: storesProp,
|
|
113
|
+
store: storeProp,
|
|
114
|
+
storeName: storeNameProp,
|
|
115
|
+
plotId,
|
|
116
|
+
register: true,
|
|
117
|
+
}), [storeNameProp, storeProp, storesProp, plotId]);
|
|
121
118
|
|
|
122
119
|
const [supportsWebGpu, supportsWebGpuMessage] = useMemo(checkWebGpuFeatureDetection, []);
|
|
123
120
|
|
|
@@ -129,7 +126,7 @@ export function Pluot(props) {
|
|
|
129
126
|
|
|
130
127
|
// We may want to update these things without triggering a re-render.
|
|
131
128
|
const isRenderingRef = useRef(false);
|
|
132
|
-
const currentTimeout = useRef(
|
|
129
|
+
const currentTimeout = useRef(minTimeout);
|
|
133
130
|
|
|
134
131
|
// TODO: do we want to use the backlog approach or not?
|
|
135
132
|
// (Similar to the one used in the Vitessce heatmap)
|
|
@@ -143,12 +140,12 @@ export function Pluot(props) {
|
|
|
143
140
|
|
|
144
141
|
const [pickingResult, setPickingResult] = useState(null);
|
|
145
142
|
|
|
143
|
+
const progressBarId = useId();
|
|
146
144
|
|
|
147
145
|
useLayoutEffect(() => {
|
|
148
146
|
initialize().then(() => setIsWasmReady(getIsWasmReady()));
|
|
149
147
|
}, []);
|
|
150
148
|
|
|
151
|
-
|
|
152
149
|
const wheelHandler = useEffectEvent((event) => {
|
|
153
150
|
const onWheel = viewMode === "3d" ? onWheel3d : onWheel2d;
|
|
154
151
|
const nextCameraMatrix = onWheel({
|
|
@@ -183,25 +180,6 @@ export function Pluot(props) {
|
|
|
183
180
|
setCameraMatrix(nextCameraMatrix);
|
|
184
181
|
});
|
|
185
182
|
|
|
186
|
-
// Set up FUNCTIONAL camera.
|
|
187
|
-
useEffect(() => {
|
|
188
|
-
// Set up the camera.
|
|
189
|
-
const cameraEl = cameraElementRef.current;
|
|
190
|
-
|
|
191
|
-
if (!cameraEl) {
|
|
192
|
-
return () => {};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Create a 2D camera for handling zoom and pan.
|
|
196
|
-
cameraEl.addEventListener("mousemove", mouseMoveHandler);
|
|
197
|
-
cameraEl.addEventListener("wheel", wheelHandler);
|
|
198
|
-
|
|
199
|
-
return () => {
|
|
200
|
-
cameraEl.removeEventListener("mousemove", mouseMoveHandler);
|
|
201
|
-
cameraEl.removeEventListener("wheel", wheelHandler);
|
|
202
|
-
};
|
|
203
|
-
}, [viewMode]);
|
|
204
|
-
|
|
205
183
|
// The picking callback.
|
|
206
184
|
const pickFrame = useEffectEvent(async (screenCoordX, screenCoordY) => {
|
|
207
185
|
const renderParams = {
|
|
@@ -221,7 +199,7 @@ export function Pluot(props) {
|
|
|
221
199
|
camera_view: cameraMatrix,
|
|
222
200
|
plot_id: plotId,
|
|
223
201
|
plot_type: plotType,
|
|
224
|
-
|
|
202
|
+
stores,
|
|
225
203
|
plot_params: plotParams,
|
|
226
204
|
// Reduce the timeout value to improve responsiveness during data loading (bailed-early renders)?
|
|
227
205
|
timeout: currentTimeout.current, // in ms // Note: will not have any effect when wait_for_store_gets is false.
|
|
@@ -233,6 +211,8 @@ export function Pluot(props) {
|
|
|
233
211
|
|
|
234
212
|
const layerHeight = height - marginTop - marginBottom;
|
|
235
213
|
|
|
214
|
+
// TODO: wrap pick_wasm in a try/catch
|
|
215
|
+
|
|
236
216
|
setPickingResult(normalizePickingResult(await pick_wasm(
|
|
237
217
|
renderParams,
|
|
238
218
|
// The coordinates are relative to the "layer" (the camera region), not the full width/height.
|
|
@@ -242,6 +222,33 @@ export function Pluot(props) {
|
|
|
242
222
|
)));
|
|
243
223
|
});
|
|
244
224
|
|
|
225
|
+
// Set up the camera and picking handlers.
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
const cameraEl = cameraElementRef.current;
|
|
228
|
+
|
|
229
|
+
if (!cameraEl) {
|
|
230
|
+
return () => {};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Create a 2D camera for handling zoom and pan.
|
|
234
|
+
cameraEl.addEventListener("mousemove", mouseMoveHandler);
|
|
235
|
+
cameraEl.addEventListener("wheel", wheelHandler);
|
|
236
|
+
|
|
237
|
+
// Set up an onClick handler for picking.
|
|
238
|
+
const clickHandler = (event) => {
|
|
239
|
+
if (enablePicking) {
|
|
240
|
+
pickFrame(event.offsetX, event.offsetY);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
cameraEl.addEventListener("click", clickHandler);
|
|
244
|
+
|
|
245
|
+
return () => {
|
|
246
|
+
cameraEl.removeEventListener("mousemove", mouseMoveHandler);
|
|
247
|
+
cameraEl.removeEventListener("wheel", wheelHandler);
|
|
248
|
+
cameraEl.removeEventListener("click", clickHandler);
|
|
249
|
+
};
|
|
250
|
+
}, [viewMode, enablePicking]);
|
|
251
|
+
|
|
245
252
|
|
|
246
253
|
// The renderFrame callback.
|
|
247
254
|
// We use useEffectEvent because we want to "see"
|
|
@@ -267,7 +274,7 @@ export function Pluot(props) {
|
|
|
267
274
|
camera_view: cameraMatrix,
|
|
268
275
|
plot_id: plotId,
|
|
269
276
|
plot_type: plotType,
|
|
270
|
-
|
|
277
|
+
stores,
|
|
271
278
|
plot_params: plotParams,
|
|
272
279
|
// Reduce the timeout value to improve responsiveness during data loading (bailed-early renders)?
|
|
273
280
|
timeout: currentTimeout.current, // in ms // Note: will not have any effect when wait_for_store_gets is false.
|
|
@@ -323,23 +330,34 @@ export function Pluot(props) {
|
|
|
323
330
|
|
|
324
331
|
const frameBailedEarly = arr.at(-1) === 1;
|
|
325
332
|
if (frameBailedEarly) {
|
|
326
|
-
|
|
333
|
+
// We multiply the current timeout by two to implement an exponential backoff
|
|
334
|
+
// while the Rust side is bailing early.
|
|
335
|
+
// A downstream useEffect restarts the exponential backoff from scratch
|
|
336
|
+
// if any other plotting parameters change.
|
|
337
|
+
currentTimeout.current = Math.min(currentTimeout.current * 2, maxTimeout);
|
|
327
338
|
incBacklogIteration(); // Increment this to force a re-render.
|
|
328
339
|
setBailedEarly(true); // Update this to show the loading indicator.
|
|
329
340
|
} else {
|
|
330
341
|
// Successful render.
|
|
342
|
+
currentTimeout.current = minTimeout;
|
|
331
343
|
setBailedEarly(false); // Update this to hide the loading indicator.
|
|
332
344
|
|
|
333
345
|
// Clear the LRU cache for the store (via its store_name) corresponding to the rendered plot.
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
storeUsed.clearCache
|
|
337
|
-
|
|
346
|
+
Object.keys(stores).forEach(storeName => {
|
|
347
|
+
const storeUsed = getStore(storeName);
|
|
348
|
+
if (storeUsed && storeUsed.clearCache && typeof storeUsed.clearCache === 'function') {
|
|
349
|
+
storeUsed.clearCache();
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
338
353
|
}
|
|
339
354
|
}
|
|
340
355
|
setDidFirstRender(true);
|
|
341
356
|
});
|
|
342
357
|
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
|
|
343
361
|
const throttledRender = useMemo(
|
|
344
362
|
() => throttle(
|
|
345
363
|
renderFrame,
|
|
@@ -355,6 +373,16 @@ export function Pluot(props) {
|
|
|
355
373
|
return () => throttledRender.cancel();
|
|
356
374
|
}, [throttledRender]);
|
|
357
375
|
|
|
376
|
+
// Reset the backoff timeout whenever plot parameters change so the next
|
|
377
|
+
// sequence of bailed-early renders starts from the minimum again.
|
|
378
|
+
useEffect(() => {
|
|
379
|
+
currentTimeout.current = minTimeout;
|
|
380
|
+
}, [plotId, plotType, plotParams, stores, format,
|
|
381
|
+
width, height, aspectRatioMode, aspectRatioAlignmentMode,
|
|
382
|
+
marginLeft, marginRight, marginTop, marginBottom,
|
|
383
|
+
cameraMatrix,
|
|
384
|
+
]);
|
|
385
|
+
|
|
358
386
|
// TODO: use react-query?
|
|
359
387
|
useEffect(() => {
|
|
360
388
|
if (!isWasmReady) {
|
|
@@ -372,12 +400,12 @@ export function Pluot(props) {
|
|
|
372
400
|
|
|
373
401
|
// Render on the next animation frame.
|
|
374
402
|
throttledRender();
|
|
375
|
-
}, [isWasmReady, didFirstRender, cameraMatrix, backlogIteration, plotId, plotType, plotParams,
|
|
403
|
+
}, [isWasmReady, didFirstRender, cameraMatrix, backlogIteration, plotId, plotType, plotParams, stores, format,
|
|
376
404
|
width, height, aspectRatioMode, aspectRatioAlignmentMode, marginLeft, marginRight, marginTop, marginBottom]);
|
|
377
405
|
|
|
378
406
|
return (
|
|
379
407
|
<>
|
|
380
|
-
<div style={{ width, height, position: "relative" }}>
|
|
408
|
+
<div style={{ width, height, position: "relative", backgroundColor }}>
|
|
381
409
|
{!supportsWebGpu ? (
|
|
382
410
|
<p>{supportsWebGpuMessage}</p>
|
|
383
411
|
) : null}
|
|
@@ -392,28 +420,47 @@ export function Pluot(props) {
|
|
|
392
420
|
border: `${debugMargins ? 1 : 0}px solid red`,
|
|
393
421
|
}}
|
|
394
422
|
/>
|
|
423
|
+
{bailedEarly ? (
|
|
424
|
+
<progress
|
|
425
|
+
id={progressBarId}
|
|
426
|
+
aria-label="Loading..."
|
|
427
|
+
style={{
|
|
428
|
+
bottom: 0,
|
|
429
|
+
left: 0,
|
|
430
|
+
width: '100%',
|
|
431
|
+
position: 'absolute'
|
|
432
|
+
}}
|
|
433
|
+
/>
|
|
434
|
+
) : null}
|
|
395
435
|
{isVector ? (
|
|
396
436
|
<svg
|
|
397
437
|
ref={svgRef}
|
|
398
|
-
style={{ width, height, border:
|
|
438
|
+
style={{ width, height, border: `${debugMargins ? 1 : 0}px solid black` }}
|
|
399
439
|
width={width}
|
|
400
440
|
height={height}
|
|
401
441
|
viewBox={`0 0 ${width} ${height}`}
|
|
402
442
|
xmlns="http://www.w3.org/2000/svg"
|
|
443
|
+
{...(bailedEarly ? ({
|
|
444
|
+
['aria-busy']: true,
|
|
445
|
+
['aria-describedby']: progressBarId,
|
|
446
|
+
}) : {})}
|
|
403
447
|
>
|
|
404
448
|
</svg>
|
|
405
449
|
) : (
|
|
406
450
|
<canvas
|
|
407
451
|
ref={canvasRef}
|
|
408
|
-
style={{ width, height, border:
|
|
452
|
+
style={{ width, height, border: `${debugMargins ? 1 : 0}px solid black` }}
|
|
409
453
|
width={width}
|
|
410
454
|
height={height}
|
|
455
|
+
{...(bailedEarly ? ({
|
|
456
|
+
['aria-busy']: true,
|
|
457
|
+
['aria-describedby']: progressBarId,
|
|
458
|
+
}) : {})}
|
|
459
|
+
|
|
411
460
|
/>
|
|
412
461
|
)}
|
|
462
|
+
|
|
413
463
|
</div>
|
|
414
|
-
{bailedEarly ? (
|
|
415
|
-
<p>Loading...</p>
|
|
416
|
-
) : null}
|
|
417
464
|
<button ref={tempButtonRef} style={{ display: 'none' }}>Try lookAt</button>
|
|
418
465
|
{pickingResult ? (
|
|
419
466
|
<pre>{JSON.stringify(pickingResult, null, 2)}</pre>
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export default function createCamera(element: any, options: any): {
|
|
2
|
-
view: any;
|
|
3
|
-
element: any;
|
|
4
|
-
delay: any;
|
|
5
|
-
rotateSpeed: any;
|
|
6
|
-
zoomSpeed: any;
|
|
7
|
-
translateSpeed: any;
|
|
8
|
-
flipX: boolean;
|
|
9
|
-
flipY: boolean;
|
|
10
|
-
modes: any;
|
|
11
|
-
tick: () => boolean;
|
|
12
|
-
lookAt: (center: any, eye: any, up: any) => void;
|
|
13
|
-
rotate: (pitch: any, yaw: any, roll: any) => void;
|
|
14
|
-
pan: (dx: any, dy: any, dz: any) => void;
|
|
15
|
-
translate: (dx: any, dy: any, dz: any) => void;
|
|
16
|
-
};
|
|
17
|
-
//# sourceMappingURL=3d-view-controls.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"3d-view-controls.d.ts","sourceRoot":"","sources":["../src/3d-view-controls.js"],"names":[],"mappings":"AAmBA;;;;;;;;;;;;;;;EAgOC"}
|