@vitessce/scatterplot 3.5.9 → 3.5.11

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 CHANGED
@@ -1,4 +1,4 @@
1
- import { A, E, S, a, b, f, e, d, c } from "./index-6f1ecd35.js";
1
+ import { A, E, S, a, b, f, e, d, c } from "./index-54d5c6e0.js";
2
2
  import "react";
3
3
  import "@vitessce/vit-s";
4
4
  import "react-dom";
@@ -1,4 +1,4 @@
1
- import { B as BaseDecoder } from "./index-6f1ecd35.js";
1
+ import { B as BaseDecoder } from "./index-54d5c6e0.js";
2
2
  import "react";
3
3
  import "@vitessce/vit-s";
4
4
  import "react-dom";
@@ -1,5 +1,5 @@
1
1
  import { i as inflate_1 } from "./pako.esm-68f84e2a.js";
2
- import { g as getDefaultExportFromCjs, B as BaseDecoder } from "./index-6f1ecd35.js";
2
+ import { g as getDefaultExportFromCjs, B as BaseDecoder } from "./index-54d5c6e0.js";
3
3
  import "react";
4
4
  import "@vitessce/vit-s";
5
5
  import "react-dom";
@@ -1,4 +1,4 @@
1
- import { B as BaseDecoder } from "./index-6f1ecd35.js";
1
+ import { B as BaseDecoder } from "./index-54d5c6e0.js";
2
2
  import "react";
3
3
  import "@vitessce/vit-s";
4
4
  import "react-dom";
@@ -1,4 +1,4 @@
1
- import { B as BaseDecoder } from "./index-6f1ecd35.js";
1
+ import { B as BaseDecoder } from "./index-54d5c6e0.js";
2
2
  import "react";
3
3
  import "@vitessce/vit-s";
4
4
  import "react-dom";
@@ -1,4 +1,4 @@
1
- import { B as BaseDecoder } from "./index-6f1ecd35.js";
1
+ import { B as BaseDecoder } from "./index-54d5c6e0.js";
2
2
  import "react";
3
3
  import "@vitessce/vit-s";
4
4
  import "react-dom";
@@ -1,4 +1,4 @@
1
- import { B as BaseDecoder } from "./index-6f1ecd35.js";
1
+ import { B as BaseDecoder } from "./index-54d5c6e0.js";
2
2
  import "react";
3
3
  import "@vitessce/vit-s";
4
4
  import "react-dom";
@@ -1 +1 @@
1
- {"version":3,"file":"Scatterplot.d.ts","sourceRoot":"","sources":["../src/Scatterplot.js"],"names":[],"mappings":";AA+hBA;;;;;;GAMG;AACH,sCAKG"}
1
+ {"version":3,"file":"Scatterplot.d.ts","sourceRoot":"","sources":["../src/Scatterplot.js"],"names":[],"mappings":";AA0kBA;;;;;;GAMG;AACH,sCAKG"}
@@ -3,7 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import React, { forwardRef } from 'react';
4
4
  import { forceSimulation } from 'd3-force';
5
5
  import { isEqual } from 'lodash-es';
6
- import { deck, getSelectionLayer, ScaledExpressionExtension, SelectionExtension, } from '@vitessce/gl';
6
+ import { deck, getSelectionLayer, ScaledExpressionExtension, SelectionExtension, ContourLayerWithText, } from '@vitessce/gl';
7
7
  import { getDefaultColor } from '@vitessce/utils';
8
8
  import { AbstractSpatialOrScatterplot, createQuadTree, forceCollideRects, getOnHoverCallback, } from './shared-spatial-scatterplot/index.js';
9
9
  const CELLS_LAYER_ID = 'scatterplot';
@@ -41,7 +41,7 @@ const getPosition = (object, { index, data, target }) => {
41
41
  target[2] = POINT_LAYER_Z_INDEX;
42
42
  return target;
43
43
  };
44
- const contourGetWeight = (object, { index, data }) => data.src.featureValues[index];
44
+ const contourGetWeight = (object, { index, data }) => data.src.featureValues?.[index];
45
45
  const contourGetPosition = (object, { index, data, target }) => {
46
46
  target[0] = data.src.embeddingX[index];
47
47
  target[1] = -data.src.embeddingY[index];
@@ -108,7 +108,11 @@ class Scatterplot extends AbstractSpatialOrScatterplot {
108
108
  // - Array of per-selected-sampleSet layers (filter to sampleSet members)
109
109
  // - Array of per-sampleSet, per-obsSet layers (filter to sampleSet and obsSet members)
110
110
  createContourLayers() {
111
- const { theme, obsSetColor, sampleSetColor, contourColorEncoding, contourThresholds, contoursFilled, contourColor: contourColorProp, } = this.props;
111
+ const { theme, obsSetColor, sampleSetColor, contourColorEncoding, contourThresholds, contoursFilled, contourColor: contourColorProp, circleInfo, cellSetLabelsVisible, cellSetLabelSize, featureSelection, } = this.props;
112
+ const circlePointSet = new Set();
113
+ const [getWeight, aggregation] = Array.isArray(featureSelection) && featureSelection.length > 0
114
+ ? ([contourGetWeight, 'MEAN'])
115
+ : ([1, 'COUNT']);
112
116
  const layers = Array.from(this.stratifiedData.entries())
113
117
  .flatMap(([obsSetKey, sampleSetMap]) => Array.from(sampleSetMap.entries())
114
118
  .map(([sampleSetKey, arrs]) => {
@@ -127,30 +131,34 @@ class Scatterplot extends AbstractSpatialOrScatterplot {
127
131
  contourColor = (sampleSetColor?.find(d => isEqual(sampleSetKey, d.path))?.color
128
132
  || contourColor);
129
133
  }
130
- return new deck.ContourLayer({
134
+ const contours = contourThresholds.map((threshold, i) => ({
135
+ i,
136
+ threshold: (contoursFilled ? [threshold, threshold[i + 1] || Infinity] : threshold),
137
+ // TODO: should the opacity steps be uniform? Should align with human perception.
138
+ // TODO: support usage of static colors.
139
+ color: [
140
+ // r, g, b
141
+ ...contourColor,
142
+ // a
143
+ (contoursFilled
144
+ ? ((i + 0.5) / contourThresholds.length * 255)
145
+ : ((i + 1) / (contourThresholds.length)) * 255),
146
+ ],
147
+ strokeWidth: 2,
148
+ // We need to specify a greater z-index so that the contour layers
149
+ // will render on top of the point layer.
150
+ zIndex: POINT_LAYER_Z_INDEX + 1 + i,
151
+ }));
152
+ return new ContourLayerWithText({
131
153
  id: `contour-${JSON.stringify(obsSetKey)}-${JSON.stringify(sampleSetKey)}`,
132
154
  coordinateSystem: deck.COORDINATE_SYSTEM.CARTESIAN,
133
155
  data: deckData,
134
- getWeight: contourGetWeight,
156
+ getWeight,
135
157
  getPosition: contourGetPosition,
136
- contours: contourThresholds.map((threshold, i) => ({
137
- threshold: (contoursFilled ? [threshold, threshold[i + 1] || Infinity] : threshold),
138
- // TODO: should the opacity steps be uniform? Should align with human perception.
139
- // TODO: support usage of static colors.
140
- color: [
141
- // r, g, b
142
- ...contourColor,
143
- // a
144
- (contoursFilled
145
- ? ((i + 0.5) / contourThresholds.length * 255)
146
- : ((i + 1) / (contourThresholds.length)) * 255),
147
- ],
148
- strokeWidth: 2,
149
- // We need to specify a greater z-index so that the contour layers
150
- // will render on top of the point layer.
151
- zIndex: POINT_LAYER_Z_INDEX + 1 + i,
152
- })),
153
- aggregation: 'MEAN',
158
+ obsSetPath: obsSetKey,
159
+ sampleSetPath: sampleSetKey,
160
+ contours,
161
+ aggregation,
154
162
  gpuAggregation: true,
155
163
  visible: true,
156
164
  pickable: false,
@@ -158,6 +166,14 @@ class Scatterplot extends AbstractSpatialOrScatterplot {
158
166
  filled: contoursFilled,
159
167
  cellSize: 0.25,
160
168
  zOffset: 0.005,
169
+ // Info for text/line rendering
170
+ circleInfo,
171
+ circlePointSet,
172
+ obsSetLabelsVisible: cellSetLabelsVisible,
173
+ obsSetLabelSize: cellSetLabelSize,
174
+ updateTriggers: {
175
+ getWeight: [getWeight],
176
+ },
161
177
  });
162
178
  }));
163
179
  return layers;
@@ -349,11 +365,16 @@ class Scatterplot extends AbstractSpatialOrScatterplot {
349
365
  }
350
366
  }
351
367
  onUpdateCellSetsLayers(onlyViewStateChange) {
352
- // Because the label sizes for the force simulation depend on the zoom level,
353
- // we _could_ run the simulation every time the zoom level changes.
354
- // However, this has a performance impact in firefox.
355
- if (onlyViewStateChange) {
356
- const { viewState, cellSetLabelsVisible } = this.props;
368
+ const { viewState, cellSetLabelsVisible, embeddingContoursVisible } = this.props;
369
+ if (embeddingContoursVisible) {
370
+ // If rendering contours, we do not want to render text labels using this method,
371
+ // as the ContourLayerWithText implements its own text labeling internally.
372
+ this.cellSetsLayers = [];
373
+ }
374
+ else if (onlyViewStateChange) {
375
+ // Because the label sizes for the force simulation depend on the zoom level,
376
+ // we _could_ run the simulation every time the zoom level changes.
377
+ // However, this has a performance impact in firefox.
357
378
  const { zoom } = viewState;
358
379
  const { cellSetsLabelPrevZoom } = this;
359
380
  // Instead, we can just check if the zoom level has changed
@@ -378,6 +399,16 @@ class Scatterplot extends AbstractSpatialOrScatterplot {
378
399
  const { obsEmbeddingIndex, obsEmbedding, } = this.props;
379
400
  super.viewInfoDidUpdate(obsEmbeddingIndex, obsEmbedding, makeFlippedGetObsCoords);
380
401
  }
402
+ componentWillUnmount() {
403
+ delete this.cellsQuadTree;
404
+ delete this.cellsLayer;
405
+ delete this.cellsData;
406
+ delete this.stratifiedData;
407
+ delete this.cellSetsForceSimulation;
408
+ delete this.cellSetsLabelPrevZoom;
409
+ delete this.cellSetsLayers;
410
+ delete this.contourLayers;
411
+ }
381
412
  /**
382
413
  * Here, asynchronously check whether props have
383
414
  * updated which require re-computing memoized variables,
@@ -412,7 +443,11 @@ class Scatterplot extends AbstractSpatialOrScatterplot {
412
443
  this.onUpdateCellsLayer();
413
444
  forceUpdate = true;
414
445
  }
415
- if (['stratifiedData', 'contourColorEncoding', 'contoursFilled', 'contourThresholds', 'embeddingContoursVisible'].some(shallowDiff)) {
446
+ if ([
447
+ 'stratifiedData', 'contourColorEncoding', 'contoursFilled',
448
+ 'contourThresholds', 'embeddingContoursVisible',
449
+ 'cellSetLabelsVisible', 'cellSetLabelSize',
450
+ ].some(shallowDiff)) {
416
451
  // Cells data changed.
417
452
  this.onUpdateContourLayers();
418
453
  forceUpdate = true;
@@ -420,6 +455,7 @@ class Scatterplot extends AbstractSpatialOrScatterplot {
420
455
  if ([
421
456
  'cellSetPolygons', 'cellSetPolygonsVisible',
422
457
  'cellSetLabelsVisible', 'cellSetLabelSize',
458
+ 'embeddingContoursVisible',
423
459
  ].some(shallowDiff)) {
424
460
  // Cell sets layer props changed.
425
461
  this.onUpdateCellSetsLayers(false);
@@ -1 +1 @@
1
- {"version":3,"file":"ScatterplotOptions.d.ts","sourceRoot":"","sources":["../src/ScatterplotOptions.js"],"names":[],"mappings":"AAUA,oEAqdC"}
1
+ {"version":3,"file":"ScatterplotOptions.d.ts","sourceRoot":"","sources":["../src/ScatterplotOptions.js"],"names":[],"mappings":"AAYA,oEAufC"}
@@ -6,8 +6,9 @@ import { Checkbox, Slider, TableCell, TableRow } from '@material-ui/core';
6
6
  import { capitalize } from '@vitessce/utils';
7
7
  import { usePlotOptionsStyles, CellColorEncodingOption, OptionsContainer, OptionSelect, } from '@vitessce/vit-s';
8
8
  import { GLSL_COLORMAPS } from '@vitessce/gl';
9
+ const FEATURE_AGGREGATION_STRATEGIES = ['first', 'last', 'sum', 'mean'];
9
10
  export default function ScatterplotOptions(props) {
10
- const { children, observationsLabel, cellRadius, setCellRadius, cellRadiusMode, setCellRadiusMode, cellOpacity, setCellOpacity, cellOpacityMode, setCellOpacityMode, cellSetLabelsVisible, setCellSetLabelsVisible, tooltipsVisible, setTooltipsVisible, cellSetLabelSize, setCellSetLabelSize, cellSetPolygonsVisible, setCellSetPolygonsVisible, cellColorEncoding, setCellColorEncoding, geneExpressionColormap, setGeneExpressionColormap, geneExpressionColormapRange, setGeneExpressionColormapRange, embeddingPointsVisible, setEmbeddingPointsVisible, embeddingContoursVisible, setEmbeddingContoursVisible, embeddingContoursFilled, setEmbeddingContoursFilled, contourPercentiles, setContourPercentiles, defaultContourPercentiles, contourColorEncoding, setContourColorEncoding, } = props;
11
+ const { children, observationsLabel, cellRadius, setCellRadius, cellRadiusMode, setCellRadiusMode, cellOpacity, setCellOpacity, cellOpacityMode, setCellOpacityMode, cellSetLabelsVisible, setCellSetLabelsVisible, tooltipsVisible, setTooltipsVisible, cellSetLabelSize, setCellSetLabelSize, cellSetPolygonsVisible, setCellSetPolygonsVisible, cellColorEncoding, setCellColorEncoding, geneExpressionColormap, setGeneExpressionColormap, geneExpressionColormapRange, setGeneExpressionColormapRange, embeddingPointsVisible, setEmbeddingPointsVisible, embeddingContoursVisible, setEmbeddingContoursVisible, embeddingContoursFilled, setEmbeddingContoursFilled, contourPercentiles, setContourPercentiles, defaultContourPercentiles, contourColorEncoding, setContourColorEncoding, featureAggregationStrategy, setFeatureAggregationStrategy, } = props;
11
12
  const scatterplotOptionsId = useId();
12
13
  const observationsLabelNice = capitalize(observationsLabel);
13
14
  const classes = usePlotOptionsStyles();
@@ -58,6 +59,9 @@ export default function ScatterplotOptions(props) {
58
59
  setContourPercentiles(values);
59
60
  }
60
61
  const handlePercentilesChangeDebounced = useCallback(debounce(handlePercentilesChange, 5, { trailing: true }), [handlePercentilesChange]);
62
+ function handleFeatureAggregationStrategyChange(event) {
63
+ setFeatureAggregationStrategy(event.target.value);
64
+ }
61
65
  return (_jsxs(OptionsContainer, { children: [children, _jsx(CellColorEncodingOption, { observationsLabel: observationsLabel, cellColorEncoding: cellColorEncoding, setCellColorEncoding: setCellColorEncoding }), _jsxs(TableRow, { children: [_jsx(TableCell, { className: classes.labelCell, variant: "head", scope: "row", children: _jsxs("label", { htmlFor: `scatterplot-set-labels-visible-${scatterplotOptionsId}`, children: [observationsLabelNice, " Set Labels Visible"] }) }), _jsx(TableCell, { className: classes.inputCell, variant: "body", children: _jsx(Checkbox, { className: classes.checkbox, checked: cellSetLabelsVisible, onChange: handleLabelVisibilityChange, name: "scatterplot-option-cell-set-labels", color: "default", inputProps: {
62
66
  'aria-label': 'Show or hide set labels',
63
67
  id: `scatterplot-set-labels-visible-${scatterplotOptionsId}`,
@@ -93,5 +97,7 @@ export default function ScatterplotOptions(props) {
93
97
  id: `scatterplot-contours-filled-${scatterplotOptionsId}`,
94
98
  } }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { className: classes.labelCell, variant: "head", scope: "row", children: _jsx("label", { htmlFor: `scatterplot-contour-color-encoding-${scatterplotOptionsId}`, children: "Contour Color Encoding" }) }), _jsx(TableCell, { className: classes.inputCell, variant: "body", children: _jsxs(OptionSelect, { className: classes.select, value: contourColorEncoding, onChange: handleContourColorEncodingChange, inputProps: {
95
99
  id: `scatterplot-contour-color-encoding-${scatterplotOptionsId}`,
96
- }, children: [_jsx("option", { value: "sampleSetSelection", children: "Sample Sets" }), _jsxs("option", { value: "cellSetSelection", children: [observationsLabelNice, " Sets"] }), _jsx("option", { value: "staticColor", children: "Static Color" })] }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { className: classes.labelCell, variant: "head", scope: "row", children: _jsx("label", { htmlFor: `scatterplot-contour-percentiles-${scatterplotOptionsId}`, children: "Contour Percentiles" }) }), _jsx(TableCell, { className: classes.inputCell, variant: "body", children: _jsx(Slider, { classes: { root: classes.slider, valueLabel: classes.sliderValueLabel }, value: contourPercentiles || defaultContourPercentiles, onChange: handlePercentilesChangeDebounced, "aria-label": "Scatterplot sliders for contour percentile thresholds", id: `scatterplot-contour-percentiles-${scatterplotOptionsId}`, valueLabelDisplay: "auto", step: 0.005, min: 0.009, max: 0.999 }) })] })] }));
100
+ }, children: [_jsx("option", { value: "sampleSetSelection", children: "Sample Sets" }), _jsxs("option", { value: "cellSetSelection", children: [observationsLabelNice, " Sets"] }), _jsx("option", { value: "staticColor", children: "Static Color" })] }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { className: classes.labelCell, variant: "head", scope: "row", children: _jsx("label", { htmlFor: `scatterplot-contour-percentiles-${scatterplotOptionsId}`, children: "Contour Percentiles" }) }), _jsx(TableCell, { className: classes.inputCell, variant: "body", children: _jsx(Slider, { classes: { root: classes.slider, valueLabel: classes.sliderValueLabel }, value: contourPercentiles || defaultContourPercentiles, onChange: handlePercentilesChangeDebounced, "aria-label": "Scatterplot sliders for contour percentile thresholds", id: `scatterplot-contour-percentiles-${scatterplotOptionsId}`, valueLabelDisplay: "auto", step: 0.005, min: 0.009, max: 0.999 }) })] }), setFeatureAggregationStrategy ? (_jsxs(TableRow, { children: [_jsx(TableCell, { className: classes.labelCell, variant: "head", scope: "row", children: _jsx("label", { htmlFor: `feature-aggregation-strategy-${scatterplotOptionsId}`, children: "Feature Aggregation Strategy" }) }), _jsx(TableCell, { className: classes.inputCell, variant: "body", children: _jsx(OptionSelect, { className: classes.select, value: featureAggregationStrategy ?? 'first', onChange: handleFeatureAggregationStrategyChange, inputProps: {
101
+ id: `feature-aggregation-strategy-${scatterplotOptionsId}`,
102
+ }, children: FEATURE_AGGREGATION_STRATEGIES.map(opt => (_jsx("option", { value: opt, children: capitalize(opt) }, opt))) }) })] })) : null] }));
97
103
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-opacity.d.ts","sourceRoot":"","sources":["../../src/shared-spatial-scatterplot/dynamic-opacity.js"],"names":[],"mappings":"AAKA,mIA0BC;AAGD,uIAuBC"}
1
+ {"version":3,"file":"dynamic-opacity.d.ts","sourceRoot":"","sources":["../../src/shared-spatial-scatterplot/dynamic-opacity.js"],"names":[],"mappings":"AAKA,mIA0BC;AAGD,uIA6BC"}
@@ -24,11 +24,20 @@ export function getPointSizeDevicePixels(devicePixelRatio, zoom, xRange, yRange,
24
24
  // Reference: https://observablehq.com/@rreusser/selecting-the-right-opacity-for-2d-point-clouds
25
25
  export function getPointOpacity(zoom, xRange, yRange, width, height, numCells, avgFillDensity) {
26
26
  const N = numCells;
27
- const [minX, minY, maxX, maxY] = new deck.OrthographicView({ zoom }).makeViewport({
28
- height,
29
- width,
30
- viewState: { zoom, target: [0, 0, 0] },
31
- }).getBounds();
27
+ let minX;
28
+ let minY;
29
+ let maxX;
30
+ let maxY;
31
+ try {
32
+ [minX, minY, maxX, maxY] = new deck.OrthographicView({ zoom }).makeViewport({
33
+ height,
34
+ width,
35
+ viewState: { zoom, target: [0, 0, 0] },
36
+ }).getBounds();
37
+ }
38
+ catch {
39
+ return 1.0;
40
+ }
32
41
  const X = maxY - minY;
33
42
  const Y = maxX - minX;
34
43
  const X0 = xRange;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitessce/scatterplot",
3
- "version": "3.5.9",
3
+ "version": "3.5.11",
4
4
  "author": "HIDIVE Lab at HMS",
5
5
  "homepage": "http://vitessce.io",
6
6
  "repository": {
@@ -23,12 +23,12 @@
23
23
  "d3-quadtree": "^1.0.7",
24
24
  "lodash-es": "^4.17.21",
25
25
  "react-aria": "^3.28.0",
26
- "@vitessce/constants-internal": "3.5.9",
27
- "@vitessce/gl": "3.5.9",
28
- "@vitessce/icons": "3.5.9",
29
- "@vitessce/tooltip": "3.5.9",
30
- "@vitessce/utils": "3.5.9",
31
- "@vitessce/vit-s": "3.5.9"
26
+ "@vitessce/constants-internal": "3.5.11",
27
+ "@vitessce/gl": "3.5.11",
28
+ "@vitessce/icons": "3.5.11",
29
+ "@vitessce/tooltip": "3.5.11",
30
+ "@vitessce/utils": "3.5.11",
31
+ "@vitessce/vit-s": "3.5.11"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@testing-library/jest-dom": "^5.16.4",
@@ -4,6 +4,7 @@ import { forceSimulation } from 'd3-force';
4
4
  import { isEqual } from 'lodash-es';
5
5
  import {
6
6
  deck, getSelectionLayer, ScaledExpressionExtension, SelectionExtension,
7
+ ContourLayerWithText,
7
8
  } from '@vitessce/gl';
8
9
  import { getDefaultColor } from '@vitessce/utils';
9
10
  import {
@@ -49,7 +50,7 @@ const getPosition = (object, { index, data, target }) => {
49
50
  };
50
51
 
51
52
 
52
- const contourGetWeight = (object, { index, data }) => data.src.featureValues[index];
53
+ const contourGetWeight = (object, { index, data }) => data.src.featureValues?.[index];
53
54
 
54
55
  const contourGetPosition = (object, { index, data, target }) => {
55
56
  target[0] = data.src.embeddingX[index];
@@ -130,8 +131,17 @@ class Scatterplot extends AbstractSpatialOrScatterplot {
130
131
  contourThresholds,
131
132
  contoursFilled,
132
133
  contourColor: contourColorProp,
134
+ circleInfo,
135
+ cellSetLabelsVisible,
136
+ cellSetLabelSize,
137
+ featureSelection,
133
138
  } = this.props;
134
139
 
140
+ const circlePointSet = new Set();
141
+ const [getWeight, aggregation] = Array.isArray(featureSelection) && featureSelection.length > 0
142
+ ? ([contourGetWeight, 'MEAN'])
143
+ : ([1, 'COUNT']);
144
+
135
145
  const layers = Array.from(this.stratifiedData.entries())
136
146
  .flatMap(([obsSetKey, sampleSetMap]) => Array.from(sampleSetMap.entries())
137
147
  .map(([sampleSetKey, arrs]) => {
@@ -155,30 +165,35 @@ class Scatterplot extends AbstractSpatialOrScatterplot {
155
165
  || contourColor
156
166
  );
157
167
  }
158
- return new deck.ContourLayer({
168
+ const contours = contourThresholds.map((threshold, i) => ({
169
+ i,
170
+ threshold: (contoursFilled ? [threshold, threshold[i + 1] || Infinity] : threshold),
171
+ // TODO: should the opacity steps be uniform? Should align with human perception.
172
+ // TODO: support usage of static colors.
173
+ color: [
174
+ // r, g, b
175
+ ...contourColor,
176
+ // a
177
+ (contoursFilled
178
+ ? ((i + 0.5) / contourThresholds.length * 255)
179
+ : ((i + 1) / (contourThresholds.length)) * 255),
180
+ ],
181
+ strokeWidth: 2,
182
+ // We need to specify a greater z-index so that the contour layers
183
+ // will render on top of the point layer.
184
+ zIndex: POINT_LAYER_Z_INDEX + 1 + i,
185
+ }));
186
+
187
+ return new ContourLayerWithText({
159
188
  id: `contour-${JSON.stringify(obsSetKey)}-${JSON.stringify(sampleSetKey)}`,
160
189
  coordinateSystem: deck.COORDINATE_SYSTEM.CARTESIAN,
161
190
  data: deckData,
162
- getWeight: contourGetWeight,
191
+ getWeight,
163
192
  getPosition: contourGetPosition,
164
- contours: contourThresholds.map((threshold, i) => ({
165
- threshold: (contoursFilled ? [threshold, threshold[i + 1] || Infinity] : threshold),
166
- // TODO: should the opacity steps be uniform? Should align with human perception.
167
- // TODO: support usage of static colors.
168
- color: [
169
- // r, g, b
170
- ...contourColor,
171
- // a
172
- (contoursFilled
173
- ? ((i + 0.5) / contourThresholds.length * 255)
174
- : ((i + 1) / (contourThresholds.length)) * 255),
175
- ],
176
- strokeWidth: 2,
177
- // We need to specify a greater z-index so that the contour layers
178
- // will render on top of the point layer.
179
- zIndex: POINT_LAYER_Z_INDEX + 1 + i,
180
- })),
181
- aggregation: 'MEAN',
193
+ obsSetPath: obsSetKey,
194
+ sampleSetPath: sampleSetKey,
195
+ contours,
196
+ aggregation,
182
197
  gpuAggregation: true,
183
198
  visible: true,
184
199
  pickable: false,
@@ -186,6 +201,14 @@ class Scatterplot extends AbstractSpatialOrScatterplot {
186
201
  filled: contoursFilled,
187
202
  cellSize: 0.25,
188
203
  zOffset: 0.005,
204
+ // Info for text/line rendering
205
+ circleInfo,
206
+ circlePointSet,
207
+ obsSetLabelsVisible: cellSetLabelsVisible,
208
+ obsSetLabelSize: cellSetLabelSize,
209
+ updateTriggers: {
210
+ getWeight: [getWeight],
211
+ },
189
212
  });
190
213
  }));
191
214
  return layers;
@@ -429,30 +452,34 @@ class Scatterplot extends AbstractSpatialOrScatterplot {
429
452
  }
430
453
 
431
454
  onUpdateCellSetsLayers(onlyViewStateChange) {
432
- // Because the label sizes for the force simulation depend on the zoom level,
433
- // we _could_ run the simulation every time the zoom level changes.
434
- // However, this has a performance impact in firefox.
435
- if (onlyViewStateChange) {
436
- const { viewState, cellSetLabelsVisible } = this.props;
455
+ const { viewState, cellSetLabelsVisible, embeddingContoursVisible } = this.props;
456
+ if (embeddingContoursVisible) {
457
+ // If rendering contours, we do not want to render text labels using this method,
458
+ // as the ContourLayerWithText implements its own text labeling internally.
459
+ this.cellSetsLayers = [];
460
+ } else if (onlyViewStateChange) {
461
+ // Because the label sizes for the force simulation depend on the zoom level,
462
+ // we _could_ run the simulation every time the zoom level changes.
463
+ // However, this has a performance impact in firefox.
437
464
  const { zoom } = viewState;
438
465
  const { cellSetsLabelPrevZoom } = this;
439
466
  // Instead, we can just check if the zoom level has changed
440
467
  // by some relatively large delta, to be more conservative
441
468
  // about re-running the force simulation.
442
469
  if (cellSetLabelsVisible
443
- && (
444
- cellSetsLabelPrevZoom === null
445
- || Math.abs(cellSetsLabelPrevZoom - zoom) > LABEL_UPDATE_ZOOM_DELTA
446
- )
470
+ && (
471
+ cellSetsLabelPrevZoom === null
472
+ || Math.abs(cellSetsLabelPrevZoom - zoom) > LABEL_UPDATE_ZOOM_DELTA
473
+ )
447
474
  ) {
448
475
  this.cellSetsLayers = this.createCellSetsLayers();
449
476
  this.cellSetsLabelPrevZoom = zoom;
450
477
  }
451
478
  } else {
452
- // Otherwise, something more substantial than just
453
- // the viewState has changed, such as the label array
454
- // itself, so we always want to update the layer
455
- // in this case.
479
+ // Otherwise, something more substantial than just
480
+ // the viewState has changed, such as the label array
481
+ // itself, so we always want to update the layer
482
+ // in this case.
456
483
  this.cellSetsLayers = this.createCellSetsLayers();
457
484
  }
458
485
  }
@@ -469,6 +496,17 @@ class Scatterplot extends AbstractSpatialOrScatterplot {
469
496
  );
470
497
  }
471
498
 
499
+ componentWillUnmount() {
500
+ delete this.cellsQuadTree;
501
+ delete this.cellsLayer;
502
+ delete this.cellsData;
503
+ delete this.stratifiedData;
504
+ delete this.cellSetsForceSimulation;
505
+ delete this.cellSetsLabelPrevZoom;
506
+ delete this.cellSetsLayers;
507
+ delete this.contourLayers;
508
+ }
509
+
472
510
  /**
473
511
  * Here, asynchronously check whether props have
474
512
  * updated which require re-computing memoized variables,
@@ -507,7 +545,11 @@ class Scatterplot extends AbstractSpatialOrScatterplot {
507
545
  forceUpdate = true;
508
546
  }
509
547
 
510
- if (['stratifiedData', 'contourColorEncoding', 'contoursFilled', 'contourThresholds', 'embeddingContoursVisible'].some(shallowDiff)) {
548
+ if ([
549
+ 'stratifiedData', 'contourColorEncoding', 'contoursFilled',
550
+ 'contourThresholds', 'embeddingContoursVisible',
551
+ 'cellSetLabelsVisible', 'cellSetLabelSize',
552
+ ].some(shallowDiff)) {
511
553
  // Cells data changed.
512
554
  this.onUpdateContourLayers();
513
555
  forceUpdate = true;
@@ -516,6 +558,7 @@ class Scatterplot extends AbstractSpatialOrScatterplot {
516
558
  if ([
517
559
  'cellSetPolygons', 'cellSetPolygonsVisible',
518
560
  'cellSetLabelsVisible', 'cellSetLabelSize',
561
+ 'embeddingContoursVisible',
519
562
  ].some(shallowDiff)) {
520
563
  // Cell sets layer props changed.
521
564
  this.onUpdateCellSetsLayers(false);
@@ -8,6 +8,8 @@ import {
8
8
  } from '@vitessce/vit-s';
9
9
  import { GLSL_COLORMAPS } from '@vitessce/gl';
10
10
 
11
+ const FEATURE_AGGREGATION_STRATEGIES = ['first', 'last', 'sum', 'mean'];
12
+
11
13
  export default function ScatterplotOptions(props) {
12
14
  const {
13
15
  children,
@@ -48,6 +50,9 @@ export default function ScatterplotOptions(props) {
48
50
 
49
51
  contourColorEncoding,
50
52
  setContourColorEncoding,
53
+
54
+ featureAggregationStrategy,
55
+ setFeatureAggregationStrategy,
51
56
  } = props;
52
57
 
53
58
  const scatterplotOptionsId = useId();
@@ -124,6 +129,10 @@ export default function ScatterplotOptions(props) {
124
129
  [handlePercentilesChange],
125
130
  );
126
131
 
132
+ function handleFeatureAggregationStrategyChange(event) {
133
+ setFeatureAggregationStrategy(event.target.value);
134
+ }
135
+
127
136
  return (
128
137
  <OptionsContainer>
129
138
  {children}
@@ -475,6 +484,33 @@ export default function ScatterplotOptions(props) {
475
484
  />
476
485
  </TableCell>
477
486
  </TableRow>
487
+ {setFeatureAggregationStrategy ? (
488
+ <TableRow>
489
+ <TableCell className={classes.labelCell} variant="head" scope="row">
490
+ <label
491
+ htmlFor={`feature-aggregation-strategy-${scatterplotOptionsId}`}
492
+ >
493
+ Feature Aggregation Strategy
494
+ </label>
495
+ </TableCell>
496
+ <TableCell className={classes.inputCell} variant="body">
497
+ <OptionSelect
498
+ className={classes.select}
499
+ value={featureAggregationStrategy ?? 'first'}
500
+ onChange={handleFeatureAggregationStrategyChange}
501
+ inputProps={{
502
+ id: `feature-aggregation-strategy-${scatterplotOptionsId}`,
503
+ }}
504
+ >
505
+ {FEATURE_AGGREGATION_STRATEGIES.map(opt => (
506
+ <option key={opt} value={opt}>
507
+ {capitalize(opt)}
508
+ </option>
509
+ ))}
510
+ </OptionSelect>
511
+ </TableCell>
512
+ </TableRow>
513
+ ) : null}
478
514
  </OptionsContainer>
479
515
  );
480
516
  }
@@ -34,11 +34,17 @@ export function getPointSizeDevicePixels(devicePixelRatio, zoom, xRange, yRange,
34
34
  // Reference: https://observablehq.com/@rreusser/selecting-the-right-opacity-for-2d-point-clouds
35
35
  export function getPointOpacity(zoom, xRange, yRange, width, height, numCells, avgFillDensity) {
36
36
  const N = numCells;
37
- const [minX, minY, maxX, maxY] = new deck.OrthographicView({ zoom }).makeViewport({
38
- height,
39
- width,
40
- viewState: { zoom, target: [0, 0, 0] },
41
- }).getBounds();
37
+ let minX; let minY; let maxX; let
38
+ maxY;
39
+ try {
40
+ [minX, minY, maxX, maxY] = new deck.OrthographicView({ zoom }).makeViewport({
41
+ height,
42
+ width,
43
+ viewState: { zoom, target: [0, 0, 0] },
44
+ }).getBounds();
45
+ } catch {
46
+ return 1.0;
47
+ }
42
48
  const X = maxY - minY;
43
49
  const Y = maxX - minX;
44
50
  const X0 = xRange;