@vitessce/scatterplot 2.0.2 → 2.0.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.
Files changed (40) hide show
  1. package/dist/deflate.65a17097.mjs +13 -0
  2. package/dist/index.8206952d.mjs +132343 -0
  3. package/dist/index.mjs +15 -0
  4. package/dist/jpeg.4221d32f.mjs +840 -0
  5. package/dist/lerc.8d649494.mjs +1943 -0
  6. package/dist/lzw.89350f4e.mjs +128 -0
  7. package/dist/packbits.986f9d9f.mjs +30 -0
  8. package/dist/pako.esm.4b234125.mjs +3940 -0
  9. package/dist/raw.1cc73933.mjs +12 -0
  10. package/dist/webimage.be69a2d5.mjs +32 -0
  11. package/{dist → dist-tsc}/index.js +0 -0
  12. package/package.json +11 -11
  13. package/src/EmptyMessage.js +11 -0
  14. package/src/Scatterplot.js +385 -0
  15. package/src/ScatterplotOptions.js +247 -0
  16. package/src/ScatterplotTooltipSubscriber.js +38 -0
  17. package/src/index.js +11 -0
  18. package/src/shared-spatial-scatterplot/AbstractSpatialOrScatterplot.js +274 -0
  19. package/src/shared-spatial-scatterplot/ToolMenu.js +105 -0
  20. package/src/shared-spatial-scatterplot/ToolMenu.test.jsx +18 -0
  21. package/src/shared-spatial-scatterplot/cursor.js +23 -0
  22. package/src/shared-spatial-scatterplot/dynamic-opacity.js +58 -0
  23. package/src/shared-spatial-scatterplot/dynamic-opacity.test.js +33 -0
  24. package/src/shared-spatial-scatterplot/force-collide-rects.js +189 -0
  25. package/src/shared-spatial-scatterplot/force-collide-rects.test.js +72 -0
  26. package/{dist → src}/shared-spatial-scatterplot/index.js +4 -1
  27. package/src/shared-spatial-scatterplot/quadtree.js +27 -0
  28. package/dist/EmptyMessage.js +0 -6
  29. package/dist/Scatterplot.js +0 -304
  30. package/dist/ScatterplotOptions.js +0 -50
  31. package/dist/ScatterplotTooltipSubscriber.js +0 -14
  32. package/dist/shared-spatial-scatterplot/AbstractSpatialOrScatterplot.js +0 -213
  33. package/dist/shared-spatial-scatterplot/ToolMenu.js +0 -58
  34. package/dist/shared-spatial-scatterplot/ToolMenu.test.js +0 -16
  35. package/dist/shared-spatial-scatterplot/cursor.js +0 -22
  36. package/dist/shared-spatial-scatterplot/dynamic-opacity.js +0 -47
  37. package/dist/shared-spatial-scatterplot/dynamic-opacity.test.js +0 -28
  38. package/dist/shared-spatial-scatterplot/force-collide-rects.js +0 -169
  39. package/dist/shared-spatial-scatterplot/force-collide-rects.test.js +0 -58
  40. package/dist/shared-spatial-scatterplot/quadtree.js +0 -26
@@ -0,0 +1,12 @@
1
+ import { B as BaseDecoder } from "./index.8206952d.mjs";
2
+ import "react";
3
+ import "@vitessce/vit-s";
4
+ import "react-dom";
5
+ class RawDecoder extends BaseDecoder {
6
+ decodeBlock(buffer) {
7
+ return buffer;
8
+ }
9
+ }
10
+ export {
11
+ RawDecoder as default
12
+ };
@@ -0,0 +1,32 @@
1
+ import { B as BaseDecoder } from "./index.8206952d.mjs";
2
+ import "react";
3
+ import "@vitessce/vit-s";
4
+ import "react-dom";
5
+ class WebImageDecoder extends BaseDecoder {
6
+ constructor() {
7
+ super();
8
+ if (typeof createImageBitmap === "undefined") {
9
+ throw new Error("Cannot decode WebImage as `createImageBitmap` is not available");
10
+ } else if (typeof document === "undefined" && typeof OffscreenCanvas === "undefined") {
11
+ throw new Error("Cannot decode WebImage as neither `document` nor `OffscreenCanvas` is not available");
12
+ }
13
+ }
14
+ async decode(fileDirectory, buffer) {
15
+ const blob = new Blob([buffer]);
16
+ const imageBitmap = await createImageBitmap(blob);
17
+ let canvas;
18
+ if (typeof document !== "undefined") {
19
+ canvas = document.createElement("canvas");
20
+ canvas.width = imageBitmap.width;
21
+ canvas.height = imageBitmap.height;
22
+ } else {
23
+ canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height);
24
+ }
25
+ const ctx = canvas.getContext("2d");
26
+ ctx.drawImage(imageBitmap, 0, 0);
27
+ return ctx.getImageData(0, 0, imageBitmap.width, imageBitmap.height).data.buffer;
28
+ }
29
+ }
30
+ export {
31
+ WebImageDecoder as default
32
+ };
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitessce/scatterplot",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "author": "Gehlenborg Lab",
5
5
  "homepage": "http://vitessce.io",
6
6
  "repository": {
@@ -8,9 +8,10 @@
8
8
  "url": "git+https://github.com/vitessce/vitessce.git"
9
9
  },
10
10
  "license": "MIT",
11
- "main": "dist/index.js",
11
+ "main": "dist/index.mjs",
12
12
  "files": [
13
- "dist"
13
+ "dist",
14
+ "src"
14
15
  ],
15
16
  "dependencies": {
16
17
  "@material-ui/core": "~4.12.3",
@@ -18,12 +19,12 @@
18
19
  "d3-force": "^2.1.1",
19
20
  "d3-quadtree": "^1.0.7",
20
21
  "lodash": "^4.17.21",
21
- "@vitessce/constants-internal": "2.0.2",
22
- "@vitessce/gl": "2.0.2",
23
- "@vitessce/icons": "2.0.2",
24
- "@vitessce/tooltip": "2.0.2",
25
- "@vitessce/utils": "2.0.2",
26
- "@vitessce/vit-s": "2.0.2"
22
+ "@vitessce/constants-internal": "2.0.3",
23
+ "@vitessce/gl": "2.0.3",
24
+ "@vitessce/icons": "2.0.3",
25
+ "@vitessce/tooltip": "2.0.3",
26
+ "@vitessce/utils": "2.0.3",
27
+ "@vitessce/vit-s": "2.0.3"
27
28
  },
28
29
  "devDependencies": {
29
30
  "react": "^18.0.0",
@@ -34,8 +35,7 @@
34
35
  "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
35
36
  },
36
37
  "scripts": {
37
- "start": "tsc --watch",
38
- "build": "tsc",
38
+ "bundle": "pnpm exec vite build -c ../../../scripts/vite.config.js",
39
39
  "test": "pnpm exec vitest --run -r ../../../ --dir ."
40
40
  }
41
41
  }
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+
3
+ export default function EmptyMessage(props) {
4
+ const {
5
+ visible,
6
+ message,
7
+ } = props;
8
+ return visible ? (
9
+ <div>{message}</div>
10
+ ) : null;
11
+ }
@@ -0,0 +1,385 @@
1
+ /* eslint-disable no-param-reassign */
2
+ import React, { forwardRef } from 'react';
3
+ import { forceSimulation } from 'd3-force';
4
+ import {
5
+ deck, getSelectionLayers, ScaledExpressionExtension, SelectionExtension,
6
+ } from '@vitessce/gl';
7
+ import { getDefaultColor } from '@vitessce/utils';
8
+ import {
9
+ AbstractSpatialOrScatterplot, createQuadTree, forceCollideRects, getOnHoverCallback,
10
+ } from './shared-spatial-scatterplot';
11
+
12
+ const CELLS_LAYER_ID = 'scatterplot';
13
+ const LABEL_FONT_FAMILY = "-apple-system, 'Helvetica Neue', Arial, sans-serif";
14
+ const NUM_FORCE_SIMULATION_TICKS = 100;
15
+ const LABEL_UPDATE_ZOOM_DELTA = 0.25;
16
+
17
+ // Default getter function props.
18
+ const makeDefaultGetCellColors = (cellColors, obsIndex, theme) => (object, { index }) => {
19
+ const [r, g, b, a] = (cellColors && obsIndex && cellColors.get(obsIndex[index]))
20
+ || getDefaultColor(theme);
21
+ return [r, g, b, 255 * (a || 1)];
22
+ };
23
+ const makeDefaultGetObsCoords = obsEmbedding => i => ([
24
+ obsEmbedding.data[0][i],
25
+ obsEmbedding.data[1][i],
26
+ 0,
27
+ ]);
28
+ const makeFlippedGetObsCoords = obsEmbedding => i => ([
29
+ obsEmbedding.data[0][i],
30
+ -obsEmbedding.data[1][i],
31
+ 0,
32
+ ]);
33
+ const getPosition = (object, { index, data, target }) => {
34
+ target[0] = data.src.obsEmbedding.data[0][index];
35
+ target[1] = -data.src.obsEmbedding.data[1][index];
36
+ target[2] = 0;
37
+ return target;
38
+ };
39
+
40
+ /**
41
+ * React component which renders a scatterplot from cell data.
42
+ * @param {object} props
43
+ * @param {string} props.uuid A unique identifier for this component.
44
+ * @param {string} props.theme The current vitessce theme.
45
+ * @param {object} props.viewState The deck.gl view state.
46
+ * @param {function} props.setViewState Function to call to update the deck.gl view state.
47
+ * @param {object} props.cells
48
+ * @param {string} props.mapping The name of the coordinate mapping field,
49
+ * for each cell, for example "PCA" or "t-SNE".
50
+ * @param {Map} props.cellColors Mapping of cell IDs to colors.
51
+ * @param {array} props.cellSelection Array of selected cell IDs.
52
+ * @param {array} props.cellFilter Array of filtered cell IDs. By default, null.
53
+ * @param {number} props.cellRadius The value for `radiusScale` to pass
54
+ * to the deck.gl cells ScatterplotLayer.
55
+ * @param {number} props.cellOpacity The value for `opacity` to pass
56
+ * to the deck.gl cells ScatterplotLayer.
57
+ * @param {function} props.getCellCoords Getter function for cell coordinates
58
+ * (used by the selection layer).
59
+ * @param {function} props.getCellPosition Getter function for cell [x, y, z] position.
60
+ * @param {function} props.getCellColor Getter function for cell color as [r, g, b] array.
61
+ * @param {function} props.getExpressionValue Getter function for cell expression value.
62
+ * @param {function} props.getCellIsSelected Getter function for cell layer isSelected.
63
+ * @param {function} props.setCellSelection
64
+ * @param {function} props.setCellHighlight
65
+ * @param {function} props.updateViewInfo
66
+ * @param {function} props.onToolChange Callback for tool changes
67
+ * (lasso/pan/rectangle selection tools).
68
+ * @param {function} props.onCellClick Getter function for cell layer onClick.
69
+ */
70
+ class Scatterplot extends AbstractSpatialOrScatterplot {
71
+ constructor(props) {
72
+ super(props);
73
+
74
+ // To avoid storing large arrays/objects
75
+ // in React state, this component
76
+ // uses instance variables.
77
+ // All instance variables used in this class:
78
+ this.cellsQuadTree = null;
79
+ this.cellsLayer = null;
80
+ this.cellsData = null;
81
+ this.cellSetsForceSimulation = forceCollideRects();
82
+ this.cellSetsLabelPrevZoom = null;
83
+ this.cellSetsLayers = [];
84
+
85
+ // Initialize data and layers.
86
+ this.onUpdateCellsData();
87
+ this.onUpdateCellsLayer();
88
+ this.onUpdateCellSetsLayers();
89
+ }
90
+
91
+ createCellsLayer() {
92
+ const {
93
+ obsEmbeddingIndex: obsIndex,
94
+ theme,
95
+ cellRadius = 1.0,
96
+ cellOpacity = 1.0,
97
+ // cellFilter,
98
+ cellSelection,
99
+ setCellHighlight,
100
+ setComponentHover,
101
+ getCellIsSelected,
102
+ cellColors,
103
+ getCellColor = makeDefaultGetCellColors(cellColors, obsIndex, theme),
104
+ getExpressionValue,
105
+ onCellClick,
106
+ geneExpressionColormap,
107
+ geneExpressionColormapRange = [0.0, 1.0],
108
+ cellColorEncoding,
109
+ } = this.props;
110
+ return new deck.ScatterplotLayer({
111
+ id: CELLS_LAYER_ID,
112
+ // Note that the reference for the object passed to the data prop should not change,
113
+ // otherwise DeckGL will need to do a full re-render every time .createCellsLayer is called,
114
+ // which can be very often to handle cellOpacity and cellRadius updates for dynamic opacity.
115
+ data: this.cellsData,
116
+ coordinateSystem: deck.COORDINATE_SYSTEM.CARTESIAN,
117
+ visible: true,
118
+ pickable: true,
119
+ autoHighlight: true,
120
+ filled: true,
121
+ stroked: true,
122
+ backgroundColor: (theme === 'dark' ? [0, 0, 0] : [241, 241, 241]),
123
+ getCellIsSelected,
124
+ opacity: cellOpacity,
125
+ radiusScale: cellRadius,
126
+ radiusMinPixels: 1,
127
+ radiusMaxPixels: 30,
128
+ // Our radius pixel setters measure in pixels.
129
+ radiusUnits: 'pixels',
130
+ lineWidthUnits: 'pixels',
131
+ getPosition,
132
+ getFillColor: getCellColor,
133
+ getLineColor: getCellColor,
134
+ getRadius: 1,
135
+ getExpressionValue,
136
+ getLineWidth: 0,
137
+ extensions: [
138
+ new ScaledExpressionExtension(),
139
+ new SelectionExtension({ instanced: true }),
140
+ ],
141
+ colorScaleLo: geneExpressionColormapRange[0],
142
+ colorScaleHi: geneExpressionColormapRange[1],
143
+ isExpressionMode: (cellColorEncoding === 'geneSelection'),
144
+ colormap: geneExpressionColormap,
145
+ onClick: (info) => {
146
+ if (onCellClick) {
147
+ onCellClick(info);
148
+ }
149
+ },
150
+ onHover: getOnHoverCallback(obsIndex, setCellHighlight, setComponentHover),
151
+ updateTriggers: {
152
+ getExpressionValue,
153
+ getFillColor: [cellColorEncoding, cellSelection, cellColors],
154
+ getLineColor: [cellColorEncoding, cellSelection, cellColors],
155
+ getCellIsSelected,
156
+ },
157
+ });
158
+ }
159
+
160
+ createCellSetsLayers() {
161
+ const {
162
+ theme,
163
+ cellSetPolygons,
164
+ viewState,
165
+ cellSetPolygonsVisible,
166
+ cellSetLabelsVisible,
167
+ cellSetLabelSize,
168
+ } = this.props;
169
+
170
+ const result = [];
171
+
172
+ if (cellSetPolygonsVisible) {
173
+ result.push(new deck.PolygonLayer({
174
+ id: 'cell-sets-polygon-layer',
175
+ data: cellSetPolygons,
176
+ stroked: true,
177
+ filled: false,
178
+ wireframe: true,
179
+ lineWidthMaxPixels: 1,
180
+ getPolygon: d => d.hull,
181
+ getLineColor: d => d.color,
182
+ getLineWidth: 1,
183
+ }));
184
+ }
185
+
186
+ if (cellSetLabelsVisible) {
187
+ const { zoom } = viewState;
188
+ const nodes = cellSetPolygons.map(p => ({
189
+ x: p.centroid[0],
190
+ y: p.centroid[1],
191
+ label: p.name,
192
+ }));
193
+
194
+ const collisionForce = this.cellSetsForceSimulation
195
+ .size(d => ([
196
+ cellSetLabelSize * 1 / (2 ** zoom) * 4 * d.label.length,
197
+ cellSetLabelSize * 1 / (2 ** zoom) * 1.5,
198
+ ]));
199
+
200
+ forceSimulation()
201
+ .nodes(nodes)
202
+ .force('collision', collisionForce)
203
+ .tick(NUM_FORCE_SIMULATION_TICKS);
204
+
205
+ result.push(new deck.TextLayer({
206
+ id: 'cell-sets-text-layer',
207
+ data: nodes,
208
+ getPosition: d => ([d.x, d.y]),
209
+ getText: d => d.label,
210
+ getColor: (theme === 'dark' ? [255, 255, 255] : [0, 0, 0]),
211
+ getSize: cellSetLabelSize,
212
+ getAngle: 0,
213
+ getTextAnchor: 'middle',
214
+ getAlignmentBaseline: 'center',
215
+ fontFamily: LABEL_FONT_FAMILY,
216
+ fontWeight: 'normal',
217
+ }));
218
+ }
219
+
220
+ return result;
221
+ }
222
+
223
+ createSelectionLayers() {
224
+ const {
225
+ obsEmbeddingIndex: obsIndex,
226
+ obsEmbedding,
227
+ viewState,
228
+ setCellSelection,
229
+ } = this.props;
230
+ const { tool } = this.state;
231
+ const { cellsQuadTree } = this;
232
+ const flipYTooltip = true;
233
+ const getCellCoords = makeDefaultGetObsCoords(obsEmbedding);
234
+ return getSelectionLayers(
235
+ tool,
236
+ viewState.zoom,
237
+ CELLS_LAYER_ID,
238
+ getCellCoords,
239
+ obsIndex,
240
+ setCellSelection,
241
+ cellsQuadTree,
242
+ flipYTooltip,
243
+ );
244
+ }
245
+
246
+ getLayers() {
247
+ const {
248
+ cellsLayer,
249
+ cellSetsLayers,
250
+ } = this;
251
+ return [
252
+ cellsLayer,
253
+ ...cellSetsLayers,
254
+ ...this.createSelectionLayers(),
255
+ ];
256
+ }
257
+
258
+ onUpdateCellsData() {
259
+ const { obsEmbedding } = this.props;
260
+ if (obsEmbedding) {
261
+ const getCellCoords = makeDefaultGetObsCoords(obsEmbedding);
262
+ this.cellsQuadTree = createQuadTree(obsEmbedding, getCellCoords);
263
+ this.cellsData = {
264
+ src: {
265
+ obsEmbedding,
266
+ },
267
+ length: obsEmbedding.shape[1],
268
+ };
269
+ }
270
+ }
271
+
272
+ onUpdateCellsLayer() {
273
+ const { obsEmbeddingIndex, obsEmbedding } = this.props;
274
+ if (obsEmbeddingIndex && obsEmbedding) {
275
+ this.cellsLayer = this.createCellsLayer();
276
+ } else {
277
+ this.cellsLayer = null;
278
+ }
279
+ }
280
+
281
+ onUpdateCellSetsLayers(onlyViewStateChange) {
282
+ // Because the label sizes for the force simulation depend on the zoom level,
283
+ // we _could_ run the simulation every time the zoom level changes.
284
+ // However, this has a performance impact in firefox.
285
+ if (onlyViewStateChange) {
286
+ const { viewState, cellSetLabelsVisible } = this.props;
287
+ const { zoom } = viewState;
288
+ const { cellSetsLabelPrevZoom } = this;
289
+ // Instead, we can just check if the zoom level has changed
290
+ // by some relatively large delta, to be more conservative
291
+ // about re-running the force simulation.
292
+ if (cellSetLabelsVisible
293
+ && (
294
+ cellSetsLabelPrevZoom === null
295
+ || Math.abs(cellSetsLabelPrevZoom - zoom) > LABEL_UPDATE_ZOOM_DELTA
296
+ )
297
+ ) {
298
+ this.cellSetsLayers = this.createCellSetsLayers();
299
+ this.cellSetsLabelPrevZoom = zoom;
300
+ }
301
+ } else {
302
+ // Otherwise, something more substantial than just
303
+ // the viewState has changed, such as the label array
304
+ // itself, so we always want to update the layer
305
+ // in this case.
306
+ this.cellSetsLayers = this.createCellSetsLayers();
307
+ }
308
+ }
309
+
310
+ viewInfoDidUpdate() {
311
+ const {
312
+ obsEmbeddingIndex,
313
+ obsEmbedding,
314
+ } = this.props;
315
+ super.viewInfoDidUpdate(
316
+ obsEmbeddingIndex,
317
+ obsEmbedding,
318
+ makeFlippedGetObsCoords,
319
+ );
320
+ }
321
+
322
+ /**
323
+ * Here, asynchronously check whether props have
324
+ * updated which require re-computing memoized variables,
325
+ * followed by a re-render.
326
+ * This function does not follow React conventions or paradigms,
327
+ * it is only implemented this way to try to squeeze out
328
+ * performance.
329
+ * @param {object} prevProps The previous props to diff against.
330
+ */
331
+ componentDidUpdate(prevProps) {
332
+ this.viewInfoDidUpdate();
333
+
334
+ const shallowDiff = propName => (prevProps[propName] !== this.props[propName]);
335
+ let forceUpdate = false;
336
+ if (['obsEmbedding'].some(shallowDiff)) {
337
+ // Cells data changed.
338
+ this.onUpdateCellsData();
339
+ forceUpdate = true;
340
+ }
341
+
342
+ if ([
343
+ 'obsEmbeddingIndex', 'obsEmbedding', 'cellFilter', 'cellSelection', 'cellColors',
344
+ 'cellRadius', 'cellOpacity', 'cellRadiusMode', 'geneExpressionColormap',
345
+ 'geneExpressionColormapRange', 'geneSelection', 'cellColorEncoding',
346
+ ].some(shallowDiff)) {
347
+ // Cells layer props changed.
348
+ this.onUpdateCellsLayer();
349
+ forceUpdate = true;
350
+ }
351
+ if ([
352
+ 'cellSetPolygons', 'cellSetPolygonsVisible',
353
+ 'cellSetLabelsVisible', 'cellSetLabelSize',
354
+ ].some(shallowDiff)) {
355
+ // Cell sets layer props changed.
356
+ this.onUpdateCellSetsLayers(false);
357
+ forceUpdate = true;
358
+ }
359
+ if (shallowDiff('viewState')) {
360
+ // The viewState prop has changed (due to zoom or pan).
361
+ this.onUpdateCellSetsLayers(true);
362
+ forceUpdate = true;
363
+ }
364
+ if (forceUpdate) {
365
+ this.forceUpdate();
366
+ }
367
+ }
368
+
369
+ // render() is implemented in the abstract parent class.
370
+ }
371
+
372
+ /**
373
+ * Need this wrapper function here,
374
+ * since we want to pass a forwardRef
375
+ * so that outer components can
376
+ * access the grandchild DeckGL ref,
377
+ * but we are using a class component.
378
+ */
379
+ const ScatterplotWrapper = forwardRef((props, deckRef) => (
380
+ <Scatterplot
381
+ {...props}
382
+ deckRef={deckRef}
383
+ />
384
+ ));
385
+ export default ScatterplotWrapper;