@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/{deflate-485af92e.js → deflate-f6d73584.js} +1 -1
- package/dist/{index-6f1ecd35.js → index-54d5c6e0.js} +12719 -8193
- package/dist/index.js +1 -1
- package/dist/{jpeg-6a951bc6.js → jpeg-8d0a6c28.js} +1 -1
- package/dist/{lerc-36211a81.js → lerc-71eafedf.js} +1 -1
- package/dist/{lzw-cdee9c4e.js → lzw-7efc2185.js} +1 -1
- package/dist/{packbits-7bc2030b.js → packbits-4c889da6.js} +1 -1
- package/dist/{raw-8e26cce5.js → raw-4aa2df23.js} +1 -1
- package/dist/{webimage-d457d41a.js → webimage-19762a61.js} +1 -1
- package/dist-tsc/Scatterplot.d.ts.map +1 -1
- package/dist-tsc/Scatterplot.js +65 -29
- package/dist-tsc/ScatterplotOptions.d.ts.map +1 -1
- package/dist-tsc/ScatterplotOptions.js +8 -2
- package/dist-tsc/shared-spatial-scatterplot/dynamic-opacity.d.ts.map +1 -1
- package/dist-tsc/shared-spatial-scatterplot/dynamic-opacity.js +14 -5
- package/package.json +7 -7
- package/src/Scatterplot.js +78 -35
- package/src/ScatterplotOptions.js +36 -0
- package/src/shared-spatial-scatterplot/dynamic-opacity.js +11 -5
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-54d5c6e0.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":";AA0kBA;;;;;;GAMG;AACH,sCAKG"}
|
package/dist-tsc/Scatterplot.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
156
|
+
getWeight,
|
|
135
157
|
getPosition: contourGetPosition,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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 ([
|
|
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":"
|
|
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,
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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.
|
|
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.
|
|
27
|
-
"@vitessce/gl": "3.5.
|
|
28
|
-
"@vitessce/icons": "3.5.
|
|
29
|
-
"@vitessce/tooltip": "3.5.
|
|
30
|
-
"@vitessce/utils": "3.5.
|
|
31
|
-
"@vitessce/vit-s": "3.5.
|
|
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",
|
package/src/Scatterplot.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
191
|
+
getWeight,
|
|
163
192
|
getPosition: contourGetPosition,
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
445
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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 ([
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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;
|