@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/{deflate-dff55086.js → deflate-a9048cdd.js} +1 -1
- package/dist/{index-1a99a44a.js → index-eee2d28f.js} +5292 -2037
- package/dist/index.js +1 -1
- package/dist/{jpeg-472bcdf1.js → jpeg-7c226d63.js} +1 -1
- package/dist/{lerc-56848472.js → lerc-0384b6ad.js} +1 -1
- package/dist/{lzw-5f7284c3.js → lzw-d2cff9a8.js} +1 -1
- package/dist/{packbits-fcb8ce02.js → packbits-43302cb2.js} +1 -1
- package/dist/{raw-1772c7d9.js → raw-3ece8be3.js} +1 -1
- package/dist/{webimage-f7836afc.js → webimage-4065dcff.js} +1 -1
- package/dist-tsc/Scatterplot.d.ts.map +1 -1
- package/dist-tsc/Scatterplot.js +125 -5
- package/dist-tsc/ScatterplotOptions.d.ts.map +1 -1
- package/dist-tsc/ScatterplotOptions.js +29 -2
- package/dist-tsc/shared-spatial-scatterplot/AbstractSpatialOrScatterplot.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/Scatterplot.js +151 -4
- package/src/ScatterplotOptions.js +149 -0
package/dist/index.js
CHANGED
|
@@ -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-
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Scatterplot.d.ts","sourceRoot":"","sources":["../src/Scatterplot.js"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"Scatterplot.d.ts","sourceRoot":"","sources":["../src/Scatterplot.js"],"names":[],"mappings":";AAshBA;;;;;;GAMG;AACH,sCAKG"}
|
package/dist-tsc/Scatterplot.js
CHANGED
|
@@ -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,
|
|
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;
|
|
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.
|
|
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.
|
|
27
|
-
"@vitessce/gl": "3.4.
|
|
28
|
-
"@vitessce/
|
|
29
|
-
"@vitessce/tooltip": "3.4.
|
|
30
|
-
"@vitessce/
|
|
31
|
-
"@vitessce/vit-s": "3.4.
|
|
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",
|
package/src/Scatterplot.js
CHANGED
|
@@ -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',
|