@vitessce/scatterplot 3.4.6 → 3.4.7

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-1a99a44a.js";
1
+ import { A, E, S, a, b, f, e, d, c } from "./index-eee2d28f.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-1a99a44a.js";
1
+ import { B as BaseDecoder } from "./index-eee2d28f.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-1a99a44a.js";
2
+ import { g as getDefaultExportFromCjs, B as BaseDecoder } from "./index-eee2d28f.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-1a99a44a.js";
1
+ import { B as BaseDecoder } from "./index-eee2d28f.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-1a99a44a.js";
1
+ import { B as BaseDecoder } from "./index-eee2d28f.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-1a99a44a.js";
1
+ import { B as BaseDecoder } from "./index-eee2d28f.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-1a99a44a.js";
1
+ import { B as BaseDecoder } from "./index-eee2d28f.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":";AAmYA;;;;;;GAMG;AACH,sCAKG"}
1
+ {"version":3,"file":"Scatterplot.d.ts","sourceRoot":"","sources":["../src/Scatterplot.js"],"names":[],"mappings":";AAshBA;;;;;;GAMG;AACH,sCAKG"}
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  /* eslint-disable no-param-reassign */
3
3
  import React, { forwardRef } from 'react';
4
4
  import { forceSimulation } from 'd3-force';
5
+ import { isEqual } from 'lodash-es';
5
6
  import { deck, getSelectionLayer, ScaledExpressionExtension, SelectionExtension, } from '@vitessce/gl';
6
7
  import { getDefaultColor } from '@vitessce/utils';
7
8
  import { AbstractSpatialOrScatterplot, createQuadTree, forceCollideRects, getOnHoverCallback, } from './shared-spatial-scatterplot/index.js';
@@ -31,6 +32,14 @@ const getPosition = (object, { index, data, target }) => {
31
32
  target[2] = 0;
32
33
  return target;
33
34
  };
35
+ const contourGetWeight = (object, { index, data }) => data.src.featureValues[index];
36
+ const contourGetPosition = (object, { index, data, target }) => {
37
+ target[0] = data.src.embeddingX[index];
38
+ target[1] = -data.src.embeddingY[index];
39
+ target[2] = 0;
40
+ return target;
41
+ };
42
+ const contourGetPolygonOffset = () => ([0, 20]);
34
43
  /**
35
44
  * React component which renders a scatterplot from cell data.
36
45
  * @param {object} props
@@ -73,13 +82,75 @@ class Scatterplot extends AbstractSpatialOrScatterplot {
73
82
  this.cellsQuadTree = null;
74
83
  this.cellsLayer = null;
75
84
  this.cellsData = null;
85
+ this.stratifiedData = null;
76
86
  this.cellSetsForceSimulation = forceCollideRects();
77
87
  this.cellSetsLabelPrevZoom = null;
78
88
  this.cellSetsLayers = [];
89
+ this.contourLayers = [];
79
90
  // Initialize data and layers.
80
91
  this.onUpdateCellsData();
81
92
  this.onUpdateCellsLayer();
82
93
  this.onUpdateCellSetsLayers();
94
+ this.onUpdateStratifiedData();
95
+ this.onUpdateContourLayers();
96
+ }
97
+ // Want to support multiple types of contour layers
98
+ // - One layer for all data points (no filtering)
99
+ // - Array of per-selected-obsSet layers (filter to obsSet members)
100
+ // - Array of per-selected-sampleSet layers (filter to sampleSet members)
101
+ // - Array of per-sampleSet, per-obsSet layers (filter to sampleSet and obsSet members)
102
+ createContourLayers() {
103
+ const { theme, obsSetColor, sampleSetColor, contourColorEncoding, contourThresholds, contoursFilled, contourColor: contourColorProp, } = this.props;
104
+ const layers = Array.from(this.stratifiedData.entries())
105
+ .flatMap(([obsSetKey, sampleSetMap]) => Array.from(sampleSetMap.entries())
106
+ .map(([sampleSetKey, arrs]) => {
107
+ const deckData = arrs.get('deckData');
108
+ // The thresholds are computed based on the entire dataset,
109
+ // as opposed to just the subsets for each layer.
110
+ // This way, the contours will be comparable among different layers.
111
+ // TODO: Also support a user-defined static color.
112
+ // Fall back to default color based on the current theme.
113
+ let contourColor = contourColorProp || getDefaultColor(theme);
114
+ if (contourColorEncoding === 'cellSetSelection') {
115
+ contourColor = (obsSetColor?.find(d => isEqual(obsSetKey, d.path))?.color
116
+ || contourColor);
117
+ }
118
+ else if (contourColorEncoding === 'sampleSetSelection') {
119
+ contourColor = (sampleSetColor?.find(d => isEqual(sampleSetKey, d.path))?.color
120
+ || contourColor);
121
+ }
122
+ return new deck.ContourLayer({
123
+ id: `contour-${JSON.stringify(obsSetKey)}-${JSON.stringify(sampleSetKey)}`,
124
+ coordinateSystem: deck.COORDINATE_SYSTEM.CARTESIAN,
125
+ data: deckData,
126
+ getWeight: contourGetWeight,
127
+ getPosition: contourGetPosition,
128
+ getPolygonOffset: contourGetPolygonOffset,
129
+ contours: contourThresholds.map((threshold, i) => ({
130
+ threshold: (contoursFilled ? [threshold, threshold[i + 1] || Infinity] : threshold),
131
+ // TODO: should the opacity steps be uniform? Should align with human perception.
132
+ // TODO: support usage of static colors.
133
+ color: [
134
+ // r, g, b
135
+ ...contourColor,
136
+ // a
137
+ (contoursFilled
138
+ ? ((i + 0.5) / contourThresholds.length * 255)
139
+ : ((i + 1) / (contourThresholds.length)) * 255),
140
+ ],
141
+ strokeWidth: 2,
142
+ })),
143
+ aggregation: 'MEAN',
144
+ gpuAggregation: true,
145
+ visible: true,
146
+ pickable: false,
147
+ autoHighlight: false,
148
+ filled: contoursFilled,
149
+ cellSize: 0.25,
150
+ zOffset: 0.005,
151
+ });
152
+ }));
153
+ return layers;
83
154
  }
84
155
  createCellsLayer() {
85
156
  const { obsEmbeddingIndex: obsIndex, theme, cellRadius = 1.0, cellOpacity = 1.0,
@@ -200,9 +271,10 @@ class Scatterplot extends AbstractSpatialOrScatterplot {
200
271
  ], flipYTooltip);
201
272
  }
202
273
  getLayers() {
203
- const { cellsLayer, cellSetsLayers, } = this;
274
+ const { cellsLayer, cellSetsLayers, contourLayers, } = this;
204
275
  return [
205
276
  cellsLayer,
277
+ ...contourLayers,
206
278
  ...cellSetsLayers,
207
279
  this.createSelectionLayer(),
208
280
  ];
@@ -221,14 +293,51 @@ class Scatterplot extends AbstractSpatialOrScatterplot {
221
293
  }
222
294
  }
223
295
  onUpdateCellsLayer() {
224
- const { obsEmbeddingIndex, obsEmbedding } = this.props;
225
- if (obsEmbeddingIndex && obsEmbedding) {
296
+ const { obsEmbeddingIndex, obsEmbedding, embeddingPointsVisible } = this.props;
297
+ if (embeddingPointsVisible && obsEmbeddingIndex && obsEmbedding) {
226
298
  this.cellsLayer = this.createCellsLayer();
227
299
  }
228
300
  else {
229
301
  this.cellsLayer = null;
230
302
  }
231
303
  }
304
+ onUpdateStratifiedData() {
305
+ const { stratifiedData } = this.props;
306
+ if (stratifiedData) {
307
+ // Set up the data object { src, length } for each ContourLayer.
308
+ Array.from(stratifiedData.values())
309
+ .forEach(sampleSetMap => Array.from(sampleSetMap.values())
310
+ .forEach((arrs) => {
311
+ // Not ideal, but here we are mutating the nested Map (arrs)
312
+ // to add the 'deckData' object.
313
+ const embeddingX = arrs.get('obsEmbeddingX');
314
+ const embeddingY = arrs.get('obsEmbeddingY');
315
+ const featureValues = arrs.get('featureValue');
316
+ const obsIndex = arrs.get('obsIndex');
317
+ // We want to memoize / stabilize the object reference
318
+ // that we pass to ContourLayer.data to prevent extra re-renders.
319
+ arrs.set('deckData', {
320
+ src: {
321
+ embeddingX,
322
+ embeddingY,
323
+ featureValues,
324
+ obsIndex,
325
+ },
326
+ length: obsIndex.length,
327
+ });
328
+ }));
329
+ this.stratifiedData = stratifiedData;
330
+ }
331
+ }
332
+ onUpdateContourLayers() {
333
+ const { stratifiedData, contourThresholds, embeddingContoursVisible } = this.props;
334
+ if (embeddingContoursVisible && stratifiedData && contourThresholds) {
335
+ this.contourLayers = this.createContourLayers();
336
+ }
337
+ else {
338
+ this.contourLayers = [];
339
+ }
340
+ }
232
341
  onUpdateCellSetsLayers(onlyViewStateChange) {
233
342
  // Because the label sizes for the force simulation depend on the zoom level,
234
343
  // we _could_ run the simulation every time the zoom level changes.
@@ -266,27 +375,38 @@ class Scatterplot extends AbstractSpatialOrScatterplot {
266
375
  * This function does not follow React conventions or paradigms,
267
376
  * it is only implemented this way to try to squeeze out
268
377
  * performance.
378
+ * TODO: can this be replaced by React.memo with custom arePropsEqual?
269
379
  * @param {object} prevProps The previous props to diff against.
270
380
  */
271
381
  componentDidUpdate(prevProps) {
272
382
  this.viewInfoDidUpdate();
273
383
  const shallowDiff = propName => (prevProps[propName] !== this.props[propName]);
274
384
  let forceUpdate = false;
275
- if (['obsEmbedding'].some(shallowDiff)) {
385
+ if (['obsEmbedding', 'obsEmbeddingIndex'].some(shallowDiff)) {
276
386
  // Cells data changed.
277
387
  this.onUpdateCellsData();
278
388
  forceUpdate = true;
279
389
  }
390
+ if (['stratifiedData'].some(shallowDiff)) {
391
+ // Cells data changed.
392
+ this.onUpdateStratifiedData();
393
+ forceUpdate = true;
394
+ }
280
395
  if ([
281
396
  'obsEmbeddingIndex', 'obsEmbedding', 'cellFilter', 'cellSelection', 'cellColors',
282
397
  'cellRadius', 'cellOpacity', 'cellRadiusMode', 'geneExpressionColormap',
283
398
  'geneExpressionColormapRange', 'geneSelection', 'cellColorEncoding',
284
- 'getExpressionValue',
399
+ 'getExpressionValue', 'embeddingPointsVisible',
285
400
  ].some(shallowDiff)) {
286
401
  // Cells layer props changed.
287
402
  this.onUpdateCellsLayer();
288
403
  forceUpdate = true;
289
404
  }
405
+ if (['stratifiedData', 'contourColorEncoding', 'contoursFilled', 'contourThresholds', 'embeddingContoursVisible'].some(shallowDiff)) {
406
+ // Cells data changed.
407
+ this.onUpdateContourLayers();
408
+ forceUpdate = true;
409
+ }
290
410
  if ([
291
411
  'cellSetPolygons', 'cellSetPolygonsVisible',
292
412
  'cellSetLabelsVisible', 'cellSetLabelSize',
@@ -1 +1 @@
1
- {"version":3,"file":"ScatterplotOptions.d.ts","sourceRoot":"","sources":["../src/ScatterplotOptions.js"],"names":[],"mappings":"AAUA,oEAgUC"}
1
+ {"version":3,"file":"ScatterplotOptions.d.ts","sourceRoot":"","sources":["../src/ScatterplotOptions.js"],"names":[],"mappings":"AAUA,oEAqdC"}
@@ -7,13 +7,16 @@ 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
9
  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, } = 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
11
  const scatterplotOptionsId = useId();
12
12
  const observationsLabelNice = capitalize(observationsLabel);
13
13
  const classes = usePlotOptionsStyles();
14
14
  function handleCellRadiusModeChange(event) {
15
15
  setCellRadiusMode(event.target.value);
16
16
  }
17
+ function handleContourColorEncodingChange(event) {
18
+ setContourColorEncoding(event.target.value);
19
+ }
17
20
  function handleCellOpacityModeChange(event) {
18
21
  setCellOpacityMode(event.target.value);
19
22
  }
@@ -35,6 +38,15 @@ export default function ScatterplotOptions(props) {
35
38
  function handlePolygonVisibilityChange(event) {
36
39
  setCellSetPolygonsVisible(event.target.checked);
37
40
  }
41
+ function handlePointsVisibilityChange(event) {
42
+ setEmbeddingPointsVisible(event.target.checked);
43
+ }
44
+ function handleContoursVisibilityChange(event) {
45
+ setEmbeddingContoursVisible(event.target.checked);
46
+ }
47
+ function handleContoursFilledChange(event) {
48
+ setEmbeddingContoursFilled(event.target.checked);
49
+ }
38
50
  function handleGeneExpressionColormapChange(event) {
39
51
  setGeneExpressionColormap(event.target.value);
40
52
  }
@@ -42,6 +54,10 @@ export default function ScatterplotOptions(props) {
42
54
  setGeneExpressionColormapRange(value);
43
55
  }
44
56
  const handleColormapRangeChangeDebounced = useCallback(debounce(handleColormapRangeChange, 5, { trailing: true }), [handleColormapRangeChange]);
57
+ function handlePercentilesChange(event, values) {
58
+ setContourPercentiles(values);
59
+ }
60
+ const handlePercentilesChangeDebounced = useCallback(debounce(handlePercentilesChange, 5, { trailing: true }), [handlePercentilesChange]);
45
61
  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: {
46
62
  'aria-label': 'Show or hide set labels',
47
63
  id: `scatterplot-set-labels-visible-${scatterplotOptionsId}`,
@@ -66,5 +82,16 @@ export default function ScatterplotOptions(props) {
66
82
  }, children: GLSL_COLORMAPS.map(cmap => (_jsx("option", { value: cmap, children: cmap }, cmap))) }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { className: classes.labelCell, variant: "head", scope: "row", children: _jsx("label", { htmlFor: `scatterplot-gene-expression-colormap-range-${scatterplotOptionsId}`, children: "Gene Expression Colormap Range" }) }), _jsx(TableCell, { className: classes.inputCell, variant: "body", children: _jsx(Slider, { classes: { root: classes.slider, valueLabel: classes.sliderValueLabel }, value: geneExpressionColormapRange, onChange: handleColormapRangeChangeDebounced, getAriaLabel: (index) => {
67
83
  const labelPrefix = index === 0 ? 'Low value slider' : 'High value slider';
68
84
  return `${labelPrefix} for scatterplot gene expression colormap range`;
69
- }, id: `scatterplot-gene-expression-colormap-range-${scatterplotOptionsId}`, valueLabelDisplay: "auto", step: 0.005, min: 0.0, max: 1.0 }) })] })] }));
85
+ }, id: `scatterplot-gene-expression-colormap-range-${scatterplotOptionsId}`, valueLabelDisplay: "auto", step: 0.005, min: 0.0, max: 1.0 }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { className: classes.labelCell, variant: "head", scope: "row", children: _jsx("label", { htmlFor: `scatterplot-points-visible-${scatterplotOptionsId}`, children: "Points Visible" }) }), _jsx(TableCell, { className: classes.inputCell, variant: "body", children: _jsx(Checkbox, { className: classes.checkbox, checked: embeddingPointsVisible, onChange: handlePointsVisibilityChange, name: "scatterplot-option-point-visibility", color: "default", inputProps: {
86
+ 'aria-label': 'Show or hide scatterplot points',
87
+ id: `scatterplot-points-visible-${scatterplotOptionsId}`,
88
+ } }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { className: classes.labelCell, variant: "head", scope: "row", children: _jsx("label", { htmlFor: `scatterplot-contours-visible-${scatterplotOptionsId}`, children: "Contours Visible" }) }), _jsx(TableCell, { className: classes.inputCell, variant: "body", children: _jsx(Checkbox, { className: classes.checkbox, checked: embeddingContoursVisible, onChange: handleContoursVisibilityChange, name: "scatterplot-option-contour-visibility", color: "default", inputProps: {
89
+ 'aria-label': 'Show or hide contours',
90
+ id: `scatterplot-contours-visible-${scatterplotOptionsId}`,
91
+ } }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { className: classes.labelCell, variant: "head", scope: "row", children: _jsx("label", { htmlFor: `scatterplot-contours-filled-${scatterplotOptionsId}`, children: "Contours Filled" }) }), _jsx(TableCell, { className: classes.inputCell, variant: "body", children: _jsx(Checkbox, { className: classes.checkbox, checked: embeddingContoursFilled, onChange: handleContoursFilledChange, name: "scatterplot-option-contour-filled", color: "default", inputProps: {
92
+ 'aria-label': 'Filled or stroked contours',
93
+ id: `scatterplot-contours-filled-${scatterplotOptionsId}`,
94
+ } }) })] }), _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
+ 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 }) })] })] }));
70
97
  }
@@ -1 +1 @@
1
- {"version":3,"file":"AbstractSpatialOrScatterplot.d.ts","sourceRoot":"","sources":["../../src/shared-spatial-scatterplot/AbstractSpatialOrScatterplot.js"],"names":[],"mappings":"AAKA;;;;;GAKG;AACH;IACE,wBAeC;IAZC;;;MAGC;IAED,wBAAoB;IAStB;;;;;;;OAOG;IACH;QAF0B,SAAS,EAAxB,MAAM;aAYhB;IAED;;;;;OAKG;IACH;QAF6B,QAAQ,EAA1B,MAAM;aAIhB;IAED;;;;;OAKG;IACH,uBAFW,MAAM,QAIhB;IAED;;;;;OAKG;IACH,mBAFW,MAAM,QAQhB;IAgBD,qCAgFC;IAwCD;;MAEE;IAEF,iBAAa;IA1Ib;;;;;OAKG;IAEH,aALa,MAAM,EAAE,CAOpB;IAuFD;;;;;OAKG;IACH,iFAsBC;IAED;;OAEG;IAEH,2BAEC;IAQD;;;OAGG;IAEH,SAHa,OAAO,CAKnB;IAED;;;OAGG;IACH,sBA2DC;CACF"}
1
+ {"version":3,"file":"AbstractSpatialOrScatterplot.d.ts","sourceRoot":"","sources":["../../src/shared-spatial-scatterplot/AbstractSpatialOrScatterplot.js"],"names":[],"mappings":"AAKA;;;;;GAKG;AACH;IACE,wBAeC;IAZC;;;MAGC;IAED,wBAAoB;IAStB;;;;;;;OAOG;IACH,gDAFG;QAAuB,SAAS,EAAxB,MAAM;KAChB,QAWA;IAED;;;;;OAKG;IACH,mCAFG;QAA0B,QAAQ,EAA1B,MAAM;KAChB,QAGA;IAED;;;;;OAKG;IACH,uBAFW,MAAM,QAIhB;IAED;;;;;OAKG;IACH,mBAFW,MAAM,QAQhB;IAgBD,qCAgFC;IAwCD;;MAEE;IAEF,iBAAa;IA1Ib;;;;;OAKG;IAEH,aALa,MAAM,EAAE,CAOpB;IAuFD;;;;;OAKG;IACH,iFAsBC;IAED;;OAEG;IAEH,2BAEC;IAQD;;;OAGG;IAEH,SAHa,OAAO,CAKnB;IAED;;;OAGG;IACH,sBA2DC;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitessce/scatterplot",
3
- "version": "3.4.6",
3
+ "version": "3.4.7",
4
4
  "author": "Gehlenborg Lab",
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.4.6",
27
- "@vitessce/gl": "3.4.6",
28
- "@vitessce/utils": "3.4.6",
29
- "@vitessce/tooltip": "3.4.6",
30
- "@vitessce/icons": "3.4.6",
31
- "@vitessce/vit-s": "3.4.6"
26
+ "@vitessce/constants-internal": "3.4.7",
27
+ "@vitessce/gl": "3.4.7",
28
+ "@vitessce/icons": "3.4.7",
29
+ "@vitessce/tooltip": "3.4.7",
30
+ "@vitessce/utils": "3.4.7",
31
+ "@vitessce/vit-s": "3.4.7"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@testing-library/jest-dom": "^5.16.4",
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable no-param-reassign */
2
2
  import React, { forwardRef } from 'react';
3
3
  import { forceSimulation } from 'd3-force';
4
+ import { isEqual } from 'lodash-es';
4
5
  import {
5
6
  deck, getSelectionLayer, ScaledExpressionExtension, SelectionExtension,
6
7
  } from '@vitessce/gl';
@@ -37,6 +38,19 @@ const getPosition = (object, { index, data, target }) => {
37
38
  return target;
38
39
  };
39
40
 
41
+
42
+ const contourGetWeight = (object, { index, data }) => data.src.featureValues[index];
43
+
44
+ const contourGetPosition = (object, { index, data, target }) => {
45
+ target[0] = data.src.embeddingX[index];
46
+ target[1] = -data.src.embeddingY[index];
47
+ target[2] = 0;
48
+ return target;
49
+ };
50
+
51
+ const contourGetPolygonOffset = () => ([0, 20]);
52
+
53
+
40
54
  /**
41
55
  * React component which renders a scatterplot from cell data.
42
56
  * @param {object} props
@@ -80,14 +94,92 @@ class Scatterplot extends AbstractSpatialOrScatterplot {
80
94
  this.cellsQuadTree = null;
81
95
  this.cellsLayer = null;
82
96
  this.cellsData = null;
97
+ this.stratifiedData = null;
83
98
  this.cellSetsForceSimulation = forceCollideRects();
84
99
  this.cellSetsLabelPrevZoom = null;
85
100
  this.cellSetsLayers = [];
86
101
 
102
+ this.contourLayers = [];
103
+
87
104
  // Initialize data and layers.
88
105
  this.onUpdateCellsData();
89
106
  this.onUpdateCellsLayer();
90
107
  this.onUpdateCellSetsLayers();
108
+ this.onUpdateStratifiedData();
109
+ this.onUpdateContourLayers();
110
+ }
111
+
112
+ // Want to support multiple types of contour layers
113
+ // - One layer for all data points (no filtering)
114
+ // - Array of per-selected-obsSet layers (filter to obsSet members)
115
+ // - Array of per-selected-sampleSet layers (filter to sampleSet members)
116
+ // - Array of per-sampleSet, per-obsSet layers (filter to sampleSet and obsSet members)
117
+ createContourLayers() {
118
+ const {
119
+ theme,
120
+ obsSetColor,
121
+ sampleSetColor,
122
+ contourColorEncoding,
123
+ contourThresholds,
124
+ contoursFilled,
125
+ contourColor: contourColorProp,
126
+ } = this.props;
127
+
128
+ const layers = Array.from(this.stratifiedData.entries())
129
+ .flatMap(([obsSetKey, sampleSetMap]) => Array.from(sampleSetMap.entries())
130
+ .map(([sampleSetKey, arrs]) => {
131
+ const deckData = arrs.get('deckData');
132
+
133
+ // The thresholds are computed based on the entire dataset,
134
+ // as opposed to just the subsets for each layer.
135
+ // This way, the contours will be comparable among different layers.
136
+
137
+ // TODO: Also support a user-defined static color.
138
+ // Fall back to default color based on the current theme.
139
+ let contourColor = contourColorProp || getDefaultColor(theme);
140
+ if (contourColorEncoding === 'cellSetSelection') {
141
+ contourColor = (
142
+ obsSetColor?.find(d => isEqual(obsSetKey, d.path))?.color
143
+ || contourColor
144
+ );
145
+ } else if (contourColorEncoding === 'sampleSetSelection') {
146
+ contourColor = (
147
+ sampleSetColor?.find(d => isEqual(sampleSetKey, d.path))?.color
148
+ || contourColor
149
+ );
150
+ }
151
+ return new deck.ContourLayer({
152
+ id: `contour-${JSON.stringify(obsSetKey)}-${JSON.stringify(sampleSetKey)}`,
153
+ coordinateSystem: deck.COORDINATE_SYSTEM.CARTESIAN,
154
+ data: deckData,
155
+ getWeight: contourGetWeight,
156
+ getPosition: contourGetPosition,
157
+ getPolygonOffset: contourGetPolygonOffset,
158
+ contours: contourThresholds.map((threshold, i) => ({
159
+ threshold: (contoursFilled ? [threshold, threshold[i + 1] || Infinity] : threshold),
160
+ // TODO: should the opacity steps be uniform? Should align with human perception.
161
+ // TODO: support usage of static colors.
162
+ color: [
163
+ // r, g, b
164
+ ...contourColor,
165
+ // a
166
+ (contoursFilled
167
+ ? ((i + 0.5) / contourThresholds.length * 255)
168
+ : ((i + 1) / (contourThresholds.length)) * 255),
169
+ ],
170
+ strokeWidth: 2,
171
+ })),
172
+ aggregation: 'MEAN',
173
+ gpuAggregation: true,
174
+ visible: true,
175
+ pickable: false,
176
+ autoHighlight: false,
177
+ filled: contoursFilled,
178
+ cellSize: 0.25,
179
+ zOffset: 0.005,
180
+ });
181
+ }));
182
+ return layers;
91
183
  }
92
184
 
93
185
  createCellsLayer() {
@@ -255,9 +347,11 @@ class Scatterplot extends AbstractSpatialOrScatterplot {
255
347
  const {
256
348
  cellsLayer,
257
349
  cellSetsLayers,
350
+ contourLayers,
258
351
  } = this;
259
352
  return [
260
353
  cellsLayer,
354
+ ...contourLayers,
261
355
  ...cellSetsLayers,
262
356
  this.createSelectionLayer(),
263
357
  ];
@@ -278,14 +372,53 @@ class Scatterplot extends AbstractSpatialOrScatterplot {
278
372
  }
279
373
 
280
374
  onUpdateCellsLayer() {
281
- const { obsEmbeddingIndex, obsEmbedding } = this.props;
282
- if (obsEmbeddingIndex && obsEmbedding) {
375
+ const { obsEmbeddingIndex, obsEmbedding, embeddingPointsVisible } = this.props;
376
+ if (embeddingPointsVisible && obsEmbeddingIndex && obsEmbedding) {
283
377
  this.cellsLayer = this.createCellsLayer();
284
378
  } else {
285
379
  this.cellsLayer = null;
286
380
  }
287
381
  }
288
382
 
383
+ onUpdateStratifiedData() {
384
+ const { stratifiedData } = this.props;
385
+ if (stratifiedData) {
386
+ // Set up the data object { src, length } for each ContourLayer.
387
+ Array.from(stratifiedData.values())
388
+ .forEach(sampleSetMap => Array.from(sampleSetMap.values())
389
+ .forEach((arrs) => {
390
+ // Not ideal, but here we are mutating the nested Map (arrs)
391
+ // to add the 'deckData' object.
392
+ const embeddingX = arrs.get('obsEmbeddingX');
393
+ const embeddingY = arrs.get('obsEmbeddingY');
394
+ const featureValues = arrs.get('featureValue');
395
+ const obsIndex = arrs.get('obsIndex');
396
+
397
+ // We want to memoize / stabilize the object reference
398
+ // that we pass to ContourLayer.data to prevent extra re-renders.
399
+ arrs.set('deckData', {
400
+ src: {
401
+ embeddingX,
402
+ embeddingY,
403
+ featureValues,
404
+ obsIndex,
405
+ },
406
+ length: obsIndex.length,
407
+ });
408
+ }));
409
+ this.stratifiedData = stratifiedData;
410
+ }
411
+ }
412
+
413
+ onUpdateContourLayers() {
414
+ const { stratifiedData, contourThresholds, embeddingContoursVisible } = this.props;
415
+ if (embeddingContoursVisible && stratifiedData && contourThresholds) {
416
+ this.contourLayers = this.createContourLayers();
417
+ } else {
418
+ this.contourLayers = [];
419
+ }
420
+ }
421
+
289
422
  onUpdateCellSetsLayers(onlyViewStateChange) {
290
423
  // Because the label sizes for the force simulation depend on the zoom level,
291
424
  // we _could_ run the simulation every time the zoom level changes.
@@ -334,6 +467,7 @@ class Scatterplot extends AbstractSpatialOrScatterplot {
334
467
  * This function does not follow React conventions or paradigms,
335
468
  * it is only implemented this way to try to squeeze out
336
469
  * performance.
470
+ * TODO: can this be replaced by React.memo with custom arePropsEqual?
337
471
  * @param {object} prevProps The previous props to diff against.
338
472
  */
339
473
  componentDidUpdate(prevProps) {
@@ -341,22 +475,35 @@ class Scatterplot extends AbstractSpatialOrScatterplot {
341
475
 
342
476
  const shallowDiff = propName => (prevProps[propName] !== this.props[propName]);
343
477
  let forceUpdate = false;
344
- if (['obsEmbedding'].some(shallowDiff)) {
478
+ if (['obsEmbedding', 'obsEmbeddingIndex'].some(shallowDiff)) {
345
479
  // Cells data changed.
346
480
  this.onUpdateCellsData();
347
481
  forceUpdate = true;
348
482
  }
349
483
 
484
+ if (['stratifiedData'].some(shallowDiff)) {
485
+ // Cells data changed.
486
+ this.onUpdateStratifiedData();
487
+ forceUpdate = true;
488
+ }
489
+
350
490
  if ([
351
491
  'obsEmbeddingIndex', 'obsEmbedding', 'cellFilter', 'cellSelection', 'cellColors',
352
492
  'cellRadius', 'cellOpacity', 'cellRadiusMode', 'geneExpressionColormap',
353
493
  'geneExpressionColormapRange', 'geneSelection', 'cellColorEncoding',
354
- 'getExpressionValue',
494
+ 'getExpressionValue', 'embeddingPointsVisible',
355
495
  ].some(shallowDiff)) {
356
496
  // Cells layer props changed.
357
497
  this.onUpdateCellsLayer();
358
498
  forceUpdate = true;
359
499
  }
500
+
501
+ if (['stratifiedData', 'contourColorEncoding', 'contoursFilled', 'contourThresholds', 'embeddingContoursVisible'].some(shallowDiff)) {
502
+ // Cells data changed.
503
+ this.onUpdateContourLayers();
504
+ forceUpdate = true;
505
+ }
506
+
360
507
  if ([
361
508
  'cellSetPolygons', 'cellSetPolygonsVisible',
362
509
  'cellSetLabelsVisible', 'cellSetLabelSize',