@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.
@@ -1 +1 @@
1
- {"version":3,"file":"Pluot.d.ts","sourceRoot":"","sources":["../src/Pluot.jsx"],"names":[],"mappings":"AAiDA,uCAqXC"}
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 { FetchStore } from 'zarrita';
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 = 32, allowSimultaneousRenders = true, debugMargins = false, cameraMatrix: controlledCameraMatrix = null, setCameraMatrix: setControlledCameraMatrix = null, } = props;
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
- const storeName = useMemo(() => {
69
- if (storeNameProp) {
70
- return storeNameProp;
71
- }
72
- // If store is a string, assume it is a URL and initialize a FetchStore here.
73
- if (store) {
74
- if (typeof store === 'string') {
75
- return setStore(new FetchStore(store), plotId);
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(maxTimeout);
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
- store_name: storeName,
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
- store_name: storeName,
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
- currentTimeout.current = maxTimeout;
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
- const storeUsed = getStore(renderParams.store_name);
261
- if (storeUsed && storeUsed.clearCache && typeof storeUsed.clearCache === 'function') {
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, storeName, format,
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
- } }), isVector ? (_jsx("svg", { ref: svgRef, style: { width, height, border: "1px solid black" }, width: width, height: height, viewBox: `0 0 ${width} ${height}`, xmlns: "http://www.w3.org/2000/svg" })) : (_jsx("canvas", { ref: canvasRef, style: { width, height, border: "1px solid black" }, width: width, height: height }))] }), bailedEarly ? (_jsx("p", { children: "Loading..." })) : null, _jsx("button", { ref: tempButtonRef, style: { display: 'none' }, children: "Try lookAt" }), pickingResult ? (_jsx("pre", { children: JSON.stringify(pickingResult, null, 2) })) : null] }));
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.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.5.3",
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.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
- setStore, getStore,
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 = 32,
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
- const storeName = useMemo(() => {
109
- if (storeNameProp) {
110
- return storeNameProp;
111
- }
112
- // If store is a string, assume it is a URL and initialize a FetchStore here.
113
- if (store) {
114
- if (typeof store === 'string') {
115
- return setStore(new FetchStore(store), plotId);
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(maxTimeout);
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
- store_name: storeName,
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
- store_name: storeName,
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
- currentTimeout.current = maxTimeout;
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
- const storeUsed = getStore(renderParams.store_name);
335
- if (storeUsed && storeUsed.clearCache && typeof storeUsed.clearCache === 'function') {
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, storeName, format,
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: "1px solid black" }}
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: "1px solid black" }}
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"}