@pluot/react 0.1.0 → 0.1.2

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.
Files changed (45) hide show
  1. package/README.md +2 -0
  2. package/dist/C059-BdIta.ttf-Bdc938Uh.js +4 -0
  3. package/dist/C059-Bold.ttf-D54yVEIG.js +4 -0
  4. package/dist/C059-Italic.ttf-DLWXDpX2.js +4 -0
  5. package/dist/C059-Roman.ttf-B_4E2VG-.js +4 -0
  6. package/dist/D050000L.ttf-DThw2vhA.js +4 -0
  7. package/dist/NimbusMonoPS-Bold.ttf-DaeY0qBr.js +4 -0
  8. package/dist/NimbusMonoPS-BoldItalic.ttf-BWf8L4h2.js +4 -0
  9. package/dist/NimbusMonoPS-Italic.ttf-B6sVyRe1.js +4 -0
  10. package/dist/NimbusMonoPS-Regular.ttf-4U95hi4W.js +4 -0
  11. package/dist/NimbusRoman-Bold.ttf-CYfJBZ-D.js +4 -0
  12. package/dist/NimbusRoman-BoldItalic.ttf-IKgFHn3u.js +4 -0
  13. package/dist/NimbusRoman-Italic.ttf-DphHuaaV.js +4 -0
  14. package/dist/NimbusRoman-Regular.ttf-CyDfJ_2L.js +4 -0
  15. package/dist/NimbusSans-Bold.ttf-BgToHb0k.js +4 -0
  16. package/dist/NimbusSans-BoldItalic.ttf-DBTyNZML.js +4 -0
  17. package/dist/NimbusSans-BoldOblique.ttf-BsmbX-RQ.js +4 -0
  18. package/dist/NimbusSans-Italic.ttf-DXn1aKmU.js +4 -0
  19. package/dist/NimbusSans-Oblique.ttf-BnOkISci.js +4 -0
  20. package/dist/NimbusSans-Regular.ttf-BPUWfSNR.js +4 -0
  21. package/dist/NimbusSansNarrow-BdOblique.ttf-pMMXNfDW.js +4 -0
  22. package/dist/NimbusSansNarrow-Bold.ttf-CTqrnZ6z.js +4 -0
  23. package/dist/NimbusSansNarrow-BoldOblique.ttf-CDZKzRWR.js +4 -0
  24. package/dist/NimbusSansNarrow-Oblique.ttf-CmqouKBN.js +4 -0
  25. package/dist/NimbusSansNarrow-Regular.ttf-DOZi8TRM.js +4 -0
  26. package/dist/P052-Bold.ttf-F7VVlUHU.js +4 -0
  27. package/dist/P052-BoldItalic.ttf-CTaOaPJC.js +4 -0
  28. package/dist/P052-Italic.ttf-CfqenRHn.js +4 -0
  29. package/dist/P052-Roman.ttf-B-tZ3T0p.js +4 -0
  30. package/dist/StandardSymbolsPS.ttf-DQ9k8vPT.js +4 -0
  31. package/dist/URWBookman-Demi.ttf-gvfD6bGd.js +4 -0
  32. package/dist/URWBookman-DemiItalic.ttf-CTYueWh5.js +4 -0
  33. package/dist/URWBookman-Light.ttf-DK879rFB.js +4 -0
  34. package/dist/URWBookman-LightItalic.ttf-BWVZJTZv.js +4 -0
  35. package/dist/URWGothic-Book.ttf-BjKm3X4s.js +4 -0
  36. package/dist/URWGothic-BookOblique.ttf-UGkGTKkJ.js +4 -0
  37. package/dist/URWGothic-Demi.ttf-CUhEK02w.js +4 -0
  38. package/dist/URWGothic-DemiOblique.ttf-CP_D_bVd.js +4 -0
  39. package/dist/Z003-MediumItalic.ttf-BXc6HvHA.js +4 -0
  40. package/dist/index.js +1533 -4956
  41. package/dist-tsc/Pluot.d.ts.map +1 -1
  42. package/dist-tsc/Pluot.js +101 -195
  43. package/package.json +9 -3
  44. package/src/Pluot.jsx +118 -228
  45. package/src/index.js +1 -1
package/src/Pluot.jsx CHANGED
@@ -7,9 +7,10 @@ import {
7
7
  initialize, getIsWasmReady,
8
8
  render_wasm, pick_wasm,
9
9
  setStore, getStore,
10
- create2dCamera, create3dCamera,
11
10
  getBounds, getCameraMatrixFromBounds,
12
11
  checkWebGpuFeatureDetection,
12
+ onMouseMove2d, onWheel2d,
13
+ onMouseMove3d, onWheel3d,
13
14
  } from '@pluot/core';
14
15
 
15
16
  // Needed due to "SyntaxError: Named export 'decompressFromUint8Array' not found.
@@ -64,11 +65,43 @@ export function Pluot(props) {
64
65
  aspectRatioAlignmentMode = "Start", // "Center", "Start", "End"
65
66
  format = "Raster", // "Raster", "Vector"
66
67
  minTimeout = 32,
67
- maxTimeout = 32,
68
+ maxTimeout = 5000,
68
69
  allowSimultaneousRenders = true,
69
70
  debugMargins = false,
71
+ cameraMatrix: controlledCameraMatrix = null,
72
+ setCameraMatrix: setControlledCameraMatrix = null,
73
+ enablePicking = true,
74
+ backgroundColor = undefined,
70
75
  } = props;
71
76
 
77
+ // If cameraMatrix is not provided, then we manage the camera matrix internally.
78
+ const [uncontrolledCameraMatrix, setUncontrolledCameraMatrix] = useState(
79
+ // Note: We use an initializer function here to avoid
80
+ // sharing the same Float32Array among multiple Pluot
81
+ // component instances that may be rendered on the same page.
82
+ () => Float32Array.from(
83
+ // If the cameraMatrix prop was provided, use that for the initial camera matrix;
84
+ // otherwise use the default matrix.
85
+ controlledCameraMatrix === null
86
+ ? (viewMode === "2d" ? DEFAULT_VIEW : DEFAULT_3D_VIEW)
87
+ : controlledCameraMatrix
88
+ )
89
+ );
90
+
91
+ // Decide which camera matrix and setter to use.
92
+ // If the user provides the cameraMatrix prop but NOT the setCameraMatrix setter,
93
+ // then interpret the prop as the "initial" camera settings, but still treat as uncontrolled.
94
+ const isControlledCamera = typeof setControlledCameraMatrix === "function";
95
+ // Alternatively, if the user provides the setCameraMatrix setter, but NOT
96
+ // the cameraMatrix, interpret this as they want to use the default camera
97
+ // value initially, but they still want a controlled camera matrix.
98
+ const cameraMatrix = isControlledCamera && controlledCameraMatrix !== null
99
+ ? controlledCameraMatrix
100
+ : uncontrolledCameraMatrix;
101
+ const setCameraMatrix = isControlledCamera
102
+ ? setControlledCameraMatrix
103
+ : setUncontrolledCameraMatrix;
104
+
72
105
  const width = Math.floor(widthProp);
73
106
  const height = Math.floor(heightProp);
74
107
 
@@ -92,13 +125,13 @@ export function Pluot(props) {
92
125
 
93
126
  const svgRef = useRef(null);
94
127
  const canvasRef = useRef(null);
95
- const cameraRef = useRef(null);
128
+ const cameraElementRef = useRef(null);
96
129
 
97
130
  const tempButtonRef = useRef(null);
98
131
 
99
132
  // We may want to update these things without triggering a re-render.
100
133
  const isRenderingRef = useRef(false);
101
- const currentTimeout = useRef(maxTimeout);
134
+ const currentTimeout = useRef(minTimeout);
102
135
 
103
136
  // TODO: do we want to use the backlog approach or not?
104
137
  // (Similar to the one used in the Vitessce heatmap)
@@ -112,232 +145,45 @@ export function Pluot(props) {
112
145
 
113
146
  const [pickingResult, setPickingResult] = useState(null);
114
147
 
115
- // TODO: handle a viewMatrix that is provided and set via props,
116
- // to enable usage as a controlled component
117
- // (e.g., for linked views with shared cameras).
118
- const [viewMatrix, setViewMatrix] = useState(
119
- // Note: We use an initializer function here to avoid
120
- // sharing the same Float32Array among multiple Pluot
121
- // component instances that may be rendered on the same page.
122
- () => new Float32Array(DEFAULT_VIEW)
123
- );
124
148
 
125
149
  useLayoutEffect(() => {
126
150
  initialize().then(() => setIsWasmReady(getIsWasmReady()));
127
151
  }, []);
128
152
 
129
- useEffect(() => {
130
- // Reset view matrix on plot change.
131
- // Create a new Float32Array to avoid sharing a mutable array
132
- // among multiple Pluot component instances.
133
- setViewMatrix(new Float32Array(viewMode === "2d" ? DEFAULT_VIEW : DEFAULT_3D_VIEW));
134
- //viewMatrixRef.current = new Float32Array(DEFAULT_VIEW);
135
- }, [plotId, viewMode]);
136
-
137
153
 
138
- // Set up the camera.
139
- useEffect(() => {
140
- // Set up the camera.
141
- const cameraEl = cameraRef.current;
142
- if (!cameraEl) {
143
- return () => {};
144
- }
145
-
146
- let dispose = () => {};
147
-
148
- // Create a 2D camera for handling zoom and pan.
149
- if (viewMode === "2d") {
150
- function onCameraEvent(camera, event) {
151
- camera.tick();
152
- // Reference: https://github.com/flekschas/regl-scatterplot/blob/17a650c352fad313d1574472b2fdc5f58b9e1eca/src/index.js#L1648
153
-
154
- setViewMatrix(prev => {
155
- // Since camera events happen even on mousemove events that do not change the matrix,
156
- // we check for equality here to avoid unnecessary state updates and plot re-renders.
157
- if (isEqual(prev, camera.view)) {
158
- return prev;
159
- }
160
- return mat4.clone(camera.view)
161
- });
162
-
163
- currentTimeout.current = minTimeout;
164
- }
165
-
166
- const camera = create2dCamera(cameraEl, {
167
- isFixed: false,
168
- distance: 0.0,
169
- //target: [0.0, 0.0],
170
- //viewCenter: [0.5, 0.5], // Should this be used when the coordinate system is (0 to 1) rather than (-1 to 1)?
171
- viewCenter: [0.0, 0.0],
172
- defaultMouseDownMoveAction: "pan",
173
-
174
- onKeyDown: (event) => {
175
- onCameraEvent(camera, event);
176
- },
177
- onKeyUp: (event) => {
178
- onCameraEvent(camera, event);
179
- },
180
- onMouseDown: (event) => {
181
- onCameraEvent(camera, event);
182
- },
183
- onMouseUp: (event) => {
184
- onCameraEvent(camera, event);
185
- },
186
- onMouseMove: (event) => {
187
- onCameraEvent(camera, event);
188
- },
189
- onWheel: (event) => {
190
- onCameraEvent(camera, event);
154
+ const wheelHandler = useEffectEvent((event) => {
155
+ const onWheel = viewMode === "3d" ? onWheel3d : onWheel2d;
156
+ const nextCameraMatrix = onWheel({
157
+ width,
158
+ height,
159
+ aspectRatioMode,
160
+ aspectRatioAlignmentMode,
161
+ margins: {
162
+ marginTop,
163
+ marginBottom,
164
+ marginLeft,
165
+ marginRight,
191
166
  },
192
- aspectRatioMode: aspectRatioMode,
193
- aspectRatioAlignmentMode: aspectRatioAlignmentMode,
194
- });
195
-
196
-
197
- // Set the initial view matrix.
198
- // We need to ensure we create a new copy of the array.
199
- camera.setView(new Float32Array(viewMatrix));
200
-
201
- const tempHandler = e => {
202
- // camera.setScaleBounds([[xScaleMin, xScaleMax], [yScaleMin, yScaleMax]])
203
- //camera.lookAt([2.0, 2.0], 2.0);
204
- //onCameraEvent(camera, null);
205
-
206
- // Only zoom/pan the X axis; keep Y unchanged
207
- const nextCameraMatrix = getCameraMatrixFromBounds(
208
- { yMin: 0.0, yMax: 100.0 },
209
- new Float32Array(viewMatrix),
210
- {
211
- width,
212
- height,
213
- aspectRatioMode,
214
- aspectRatioAlignmentMode,
215
- margins: {
216
- marginTop,
217
- marginBottom,
218
- marginLeft,
219
- marginRight
220
- },
221
- },
222
- );
223
-
224
- console.log("done", nextCameraMatrix)
225
-
226
- camera.setView(nextCameraMatrix);
227
- onCameraEvent(camera, null);
228
- };
229
-
230
- tempButtonRef.current.addEventListener('click', tempHandler);
231
-
232
- // Set up an onClick handler.
233
- //
234
- const clickHandler = (event) => {
235
- pickFrame(event.offsetX, event.offsetY);
236
- };
237
- cameraEl.addEventListener("click", clickHandler);
238
-
239
- dispose = () => {
240
- camera.dispose();
241
- cameraEl.removeEventListener("click", clickHandler);
242
-
243
- tempButtonRef.current.removeEventListener('click', tempHandler);
244
- };
245
- } else if (viewMode === "3d") {
246
- function onCameraEvent(camera, event) {
247
- camera.tick();
248
- // Note: the 3D camera stores the matrix in camera.matrix (not camera.view).
249
- setViewMatrix(prev => {
250
- // Since camera events happen even on mousemove events that do not change the matrix,
251
- // we check for equality here to avoid unnecessary state updates and plot re-renders.
252
- if (isEqual(prev, camera.matrix)) {
253
- return prev;
254
- }
255
- return mat4.clone(camera.matrix)
256
- });
257
- }
258
-
259
- const camera = create3dCamera(cameraEl, {
260
- mode: "orbit",
261
- zoomSpeed: -5,
262
- });
263
-
264
- // TODO:
265
- // - fork 3d-view-controls and remove usage of "global" - then clean up vite config.
266
- // - define a camera.dispsose option.
267
-
268
- // Reference: https://github.com/flekschas/dom-2d-camera/blob/cd59ea035a0ea72c2c0535fa3721f8127946576c/src/index.js#L237C3-L315C71
269
- const keyUpHandler = (event) => {
270
- // TODO
271
- };
272
-
273
- const keyDownHandler = (event) => {
274
- // TODO
275
- };
276
-
277
- const mouseUpHandler = (event) => {
278
- // TODO
279
- };
280
-
281
- const mouseDownHandler = (event) => {
282
- // TODO
283
- };
284
-
285
- // TODO: use react state?
286
- var lastX = 0;
287
- var lastY = 0;
288
-
289
- // Reference: https://github.com/mikolalysenko/3d-view/blob/8269e02337bba1923173a750aa7f3f0f76c91ba5/example/minimal.js#L67
290
- const mouseMoveHandler = (event) => {
291
- /*
292
- var dx = (event.clientX - lastX) / width;
293
- var dy = -(event.clientY - lastY) / height;
294
- if (event.which === 1) {
295
- if (event.shiftKey) {
296
- //zoom
297
- camera.rotate(now(), 0, 0, dx);
298
- } else {
299
- //rotate
300
- camera.rotate(now(), dx, dy);
301
- }
302
- } else if (event.which === 3) {
303
- //pan
304
- camera.pan(now(), dx, dy);
305
- }
306
- lastX = event.clientX;
307
- lastY = event.clientY;
308
- */
309
- onCameraEvent(camera, event);
310
- };
311
-
312
- const wheelHandler = (event) => {
313
- //camera.pan(now(), 0, 0, event.deltaY);
314
- onCameraEvent(camera, event);
315
- };
316
-
317
- cameraEl.addEventListener("keydown", keyDownHandler);
318
- cameraEl.addEventListener("keyup", keyUpHandler);
319
- cameraEl.addEventListener("mousedown", mouseDownHandler);
320
- cameraEl.addEventListener("mouseup", mouseUpHandler);
321
- cameraEl.addEventListener("mousemove", mouseMoveHandler);
322
- cameraEl.addEventListener("wheel", wheelHandler);
323
-
324
- dispose = () => {
325
- cameraEl.removeEventListener("keydown", keyDownHandler);
326
- cameraEl.removeEventListener("keyup", keyUpHandler);
327
- cameraEl.removeEventListener("mousedown", mouseDownHandler);
328
- cameraEl.removeEventListener("mouseup", mouseUpHandler);
329
- cameraEl.removeEventListener("mousemove", mouseMoveHandler);
330
- cameraEl.removeEventListener("wheel", wheelHandler);
331
- };
332
-
333
- //camera.matrix = new Float32Array(viewMatrix);
334
-
335
- } else {
336
- throw new Error("Unknown mode found.");
337
- }
167
+ }, cameraMatrix, event);
168
+ setCameraMatrix(nextCameraMatrix);
169
+ });
338
170
 
339
- return dispose;
340
- }, [cameraRef, viewMode, aspectRatioMode, aspectRatioAlignmentMode, width, height, marginLeft, marginRight, marginTop, marginBottom]);
171
+ const mouseMoveHandler = useEffectEvent((event) => {
172
+ const onMouseMove = viewMode === "3d" ? onMouseMove3d : onMouseMove2d;
173
+ const nextCameraMatrix = onMouseMove({
174
+ width,
175
+ height,
176
+ aspectRatioMode,
177
+ aspectRatioAlignmentMode,
178
+ margins: {
179
+ marginTop,
180
+ marginBottom,
181
+ marginLeft,
182
+ marginRight,
183
+ },
184
+ }, cameraMatrix, event);
185
+ setCameraMatrix(nextCameraMatrix);
186
+ });
341
187
 
342
188
  // The picking callback.
343
189
  const pickFrame = useEffectEvent(async (screenCoordX, screenCoordY) => {
@@ -355,7 +201,7 @@ export function Pluot(props) {
355
201
  view_mode: viewMode,
356
202
  pickable: false,
357
203
  // Should see the latest viewMatrix here, since renderFrame is wrapped in useEffectEvent.
358
- camera_view: viewMatrix,
204
+ camera_view: cameraMatrix,
359
205
  plot_id: plotId,
360
206
  plot_type: plotType,
361
207
  store_name: storeName,
@@ -379,6 +225,35 @@ export function Pluot(props) {
379
225
  )));
380
226
  });
381
227
 
228
+ // Set up the camera and picking handlers.
229
+ useEffect(() => {
230
+ const cameraEl = cameraElementRef.current;
231
+
232
+ if (!cameraEl) {
233
+ return () => {};
234
+ }
235
+
236
+ // Create a 2D camera for handling zoom and pan.
237
+ cameraEl.addEventListener("mousemove", mouseMoveHandler);
238
+ cameraEl.addEventListener("wheel", wheelHandler);
239
+
240
+ // Set up an onClick handler for picking.
241
+ const clickHandler = (event) => {
242
+ if (enablePicking) {
243
+ pickFrame(event.offsetX, event.offsetY);
244
+ }
245
+ };
246
+ cameraEl.addEventListener("click", clickHandler);
247
+
248
+ return () => {
249
+ cameraEl.removeEventListener("mousemove", mouseMoveHandler);
250
+ cameraEl.removeEventListener("wheel", wheelHandler);
251
+ cameraEl.removeEventListener("click", clickHandler);
252
+ };
253
+ }, [viewMode, enablePicking]);
254
+
255
+
256
+
382
257
 
383
258
  // The renderFrame callback.
384
259
  // We use useEffectEvent because we want to "see"
@@ -401,7 +276,7 @@ export function Pluot(props) {
401
276
  view_mode: viewMode,
402
277
  pickable: false,
403
278
  // Should see the latest viewMatrix here, since renderFrame is wrapped in useEffectEvent.
404
- camera_view: viewMatrix,
279
+ camera_view: cameraMatrix,
405
280
  plot_id: plotId,
406
281
  plot_type: plotType,
407
282
  store_name: storeName,
@@ -460,11 +335,16 @@ export function Pluot(props) {
460
335
 
461
336
  const frameBailedEarly = arr.at(-1) === 1;
462
337
  if (frameBailedEarly) {
463
- currentTimeout.current = maxTimeout;
338
+ // We multiply the current timeout by two to implement an exponential backoff
339
+ // while the Rust side is bailing early.
340
+ // A downstream useEffect restarts the exponential backoff from scratch
341
+ // if any other plotting parameters change.
342
+ currentTimeout.current = Math.min(currentTimeout.current * 2, maxTimeout);
464
343
  incBacklogIteration(); // Increment this to force a re-render.
465
344
  setBailedEarly(true); // Update this to show the loading indicator.
466
345
  } else {
467
346
  // Successful render.
347
+ currentTimeout.current = minTimeout;
468
348
  setBailedEarly(false); // Update this to hide the loading indicator.
469
349
 
470
350
  // Clear the LRU cache for the store (via its store_name) corresponding to the rendered plot.
@@ -492,6 +372,16 @@ export function Pluot(props) {
492
372
  return () => throttledRender.cancel();
493
373
  }, [throttledRender]);
494
374
 
375
+ // Reset the backoff timeout whenever plot parameters change so the next
376
+ // sequence of bailed-early renders starts from the minimum again.
377
+ useEffect(() => {
378
+ currentTimeout.current = minTimeout;
379
+ }, [plotId, plotType, plotParams, storeName, format,
380
+ width, height, aspectRatioMode, aspectRatioAlignmentMode,
381
+ marginLeft, marginRight, marginTop, marginBottom,
382
+ cameraMatrix,
383
+ ]);
384
+
495
385
  // TODO: use react-query?
496
386
  useEffect(() => {
497
387
  if (!isWasmReady) {
@@ -509,17 +399,17 @@ export function Pluot(props) {
509
399
 
510
400
  // Render on the next animation frame.
511
401
  throttledRender();
512
- }, [isWasmReady, didFirstRender, viewMatrix, backlogIteration, plotId, plotType, plotParams, storeName, format,
402
+ }, [isWasmReady, didFirstRender, cameraMatrix, backlogIteration, plotId, plotType, plotParams, storeName, format,
513
403
  width, height, aspectRatioMode, aspectRatioAlignmentMode, marginLeft, marginRight, marginTop, marginBottom]);
514
404
 
515
405
  return (
516
406
  <>
517
- <div style={{ width, height, position: "relative" }}>
407
+ <div style={{ width, height, position: "relative", backgroundColor }}>
518
408
  {!supportsWebGpu ? (
519
409
  <p>{supportsWebGpuMessage}</p>
520
410
  ) : null}
521
411
  <div
522
- ref={cameraRef}
412
+ ref={cameraElementRef}
523
413
  style={{
524
414
  position: "absolute",
525
415
  top: marginTop,
package/src/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export * from '@pluot/core'; // Re-export everything from the vanilla JS package.
2
- export { Pluot } from "./Pluot.jsx";
2
+ export { Pluot } from "./Pluot.js";