@vitessce/scatterplot 2.0.3-beta.0 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/deflate.65a17097.mjs +13 -0
- package/dist/index.8206952d.mjs +132343 -0
- package/dist/index.mjs +15 -0
- package/dist/jpeg.4221d32f.mjs +840 -0
- package/dist/lerc.8d649494.mjs +1943 -0
- package/dist/lzw.89350f4e.mjs +128 -0
- package/dist/packbits.986f9d9f.mjs +30 -0
- package/dist/pako.esm.4b234125.mjs +3940 -0
- package/dist/raw.1cc73933.mjs +12 -0
- package/dist/webimage.be69a2d5.mjs +32 -0
- package/{dist → dist-tsc}/index.js +0 -0
- package/package.json +11 -11
- package/src/EmptyMessage.js +11 -0
- package/src/Scatterplot.js +385 -0
- package/src/ScatterplotOptions.js +247 -0
- package/src/ScatterplotTooltipSubscriber.js +38 -0
- package/src/index.js +11 -0
- package/src/shared-spatial-scatterplot/AbstractSpatialOrScatterplot.js +274 -0
- package/src/shared-spatial-scatterplot/ToolMenu.js +105 -0
- package/src/shared-spatial-scatterplot/ToolMenu.test.jsx +18 -0
- package/src/shared-spatial-scatterplot/cursor.js +23 -0
- package/src/shared-spatial-scatterplot/dynamic-opacity.js +58 -0
- package/src/shared-spatial-scatterplot/dynamic-opacity.test.js +33 -0
- package/src/shared-spatial-scatterplot/force-collide-rects.js +189 -0
- package/src/shared-spatial-scatterplot/force-collide-rects.test.js +72 -0
- package/{dist → src}/shared-spatial-scatterplot/index.js +4 -1
- package/src/shared-spatial-scatterplot/quadtree.js +27 -0
- package/dist/EmptyMessage.js +0 -6
- package/dist/Scatterplot.js +0 -304
- package/dist/ScatterplotOptions.js +0 -50
- package/dist/ScatterplotTooltipSubscriber.js +0 -14
- package/dist/shared-spatial-scatterplot/AbstractSpatialOrScatterplot.js +0 -213
- package/dist/shared-spatial-scatterplot/ToolMenu.js +0 -58
- package/dist/shared-spatial-scatterplot/ToolMenu.test.js +0 -16
- package/dist/shared-spatial-scatterplot/cursor.js +0 -22
- package/dist/shared-spatial-scatterplot/dynamic-opacity.js +0 -47
- package/dist/shared-spatial-scatterplot/dynamic-opacity.test.js +0 -28
- package/dist/shared-spatial-scatterplot/force-collide-rects.js +0 -169
- package/dist/shared-spatial-scatterplot/force-collide-rects.test.js +0 -58
- package/dist/shared-spatial-scatterplot/quadtree.js +0 -26
package/dist/Scatterplot.js
DELETED
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
/* eslint-disable no-param-reassign */
|
|
3
|
-
import React, { forwardRef } from 'react';
|
|
4
|
-
import { forceSimulation } from 'd3-force';
|
|
5
|
-
import { deck, getSelectionLayers, ScaledExpressionExtension, SelectionExtension, } from '@vitessce/gl';
|
|
6
|
-
import { getDefaultColor } from '@vitessce/utils';
|
|
7
|
-
import { AbstractSpatialOrScatterplot, createQuadTree, forceCollideRects, getOnHoverCallback, } from './shared-spatial-scatterplot';
|
|
8
|
-
const CELLS_LAYER_ID = 'scatterplot';
|
|
9
|
-
const LABEL_FONT_FAMILY = "-apple-system, 'Helvetica Neue', Arial, sans-serif";
|
|
10
|
-
const NUM_FORCE_SIMULATION_TICKS = 100;
|
|
11
|
-
const LABEL_UPDATE_ZOOM_DELTA = 0.25;
|
|
12
|
-
// Default getter function props.
|
|
13
|
-
const makeDefaultGetCellColors = (cellColors, obsIndex, theme) => (object, { index }) => {
|
|
14
|
-
const [r, g, b, a] = (cellColors && obsIndex && cellColors.get(obsIndex[index]))
|
|
15
|
-
|| getDefaultColor(theme);
|
|
16
|
-
return [r, g, b, 255 * (a || 1)];
|
|
17
|
-
};
|
|
18
|
-
const makeDefaultGetObsCoords = obsEmbedding => i => ([
|
|
19
|
-
obsEmbedding.data[0][i],
|
|
20
|
-
obsEmbedding.data[1][i],
|
|
21
|
-
0,
|
|
22
|
-
]);
|
|
23
|
-
const makeFlippedGetObsCoords = obsEmbedding => i => ([
|
|
24
|
-
obsEmbedding.data[0][i],
|
|
25
|
-
-obsEmbedding.data[1][i],
|
|
26
|
-
0,
|
|
27
|
-
]);
|
|
28
|
-
const getPosition = (object, { index, data, target }) => {
|
|
29
|
-
target[0] = data.src.obsEmbedding.data[0][index];
|
|
30
|
-
target[1] = -data.src.obsEmbedding.data[1][index];
|
|
31
|
-
target[2] = 0;
|
|
32
|
-
return target;
|
|
33
|
-
};
|
|
34
|
-
/**
|
|
35
|
-
* React component which renders a scatterplot from cell data.
|
|
36
|
-
* @param {object} props
|
|
37
|
-
* @param {string} props.uuid A unique identifier for this component.
|
|
38
|
-
* @param {string} props.theme The current vitessce theme.
|
|
39
|
-
* @param {object} props.viewState The deck.gl view state.
|
|
40
|
-
* @param {function} props.setViewState Function to call to update the deck.gl view state.
|
|
41
|
-
* @param {object} props.cells
|
|
42
|
-
* @param {string} props.mapping The name of the coordinate mapping field,
|
|
43
|
-
* for each cell, for example "PCA" or "t-SNE".
|
|
44
|
-
* @param {Map} props.cellColors Mapping of cell IDs to colors.
|
|
45
|
-
* @param {array} props.cellSelection Array of selected cell IDs.
|
|
46
|
-
* @param {array} props.cellFilter Array of filtered cell IDs. By default, null.
|
|
47
|
-
* @param {number} props.cellRadius The value for `radiusScale` to pass
|
|
48
|
-
* to the deck.gl cells ScatterplotLayer.
|
|
49
|
-
* @param {number} props.cellOpacity The value for `opacity` to pass
|
|
50
|
-
* to the deck.gl cells ScatterplotLayer.
|
|
51
|
-
* @param {function} props.getCellCoords Getter function for cell coordinates
|
|
52
|
-
* (used by the selection layer).
|
|
53
|
-
* @param {function} props.getCellPosition Getter function for cell [x, y, z] position.
|
|
54
|
-
* @param {function} props.getCellColor Getter function for cell color as [r, g, b] array.
|
|
55
|
-
* @param {function} props.getExpressionValue Getter function for cell expression value.
|
|
56
|
-
* @param {function} props.getCellIsSelected Getter function for cell layer isSelected.
|
|
57
|
-
* @param {function} props.setCellSelection
|
|
58
|
-
* @param {function} props.setCellHighlight
|
|
59
|
-
* @param {function} props.updateViewInfo
|
|
60
|
-
* @param {function} props.onToolChange Callback for tool changes
|
|
61
|
-
* (lasso/pan/rectangle selection tools).
|
|
62
|
-
* @param {function} props.onCellClick Getter function for cell layer onClick.
|
|
63
|
-
*/
|
|
64
|
-
class Scatterplot extends AbstractSpatialOrScatterplot {
|
|
65
|
-
constructor(props) {
|
|
66
|
-
super(props);
|
|
67
|
-
// To avoid storing large arrays/objects
|
|
68
|
-
// in React state, this component
|
|
69
|
-
// uses instance variables.
|
|
70
|
-
// All instance variables used in this class:
|
|
71
|
-
this.cellsQuadTree = null;
|
|
72
|
-
this.cellsLayer = null;
|
|
73
|
-
this.cellsData = null;
|
|
74
|
-
this.cellSetsForceSimulation = forceCollideRects();
|
|
75
|
-
this.cellSetsLabelPrevZoom = null;
|
|
76
|
-
this.cellSetsLayers = [];
|
|
77
|
-
// Initialize data and layers.
|
|
78
|
-
this.onUpdateCellsData();
|
|
79
|
-
this.onUpdateCellsLayer();
|
|
80
|
-
this.onUpdateCellSetsLayers();
|
|
81
|
-
}
|
|
82
|
-
createCellsLayer() {
|
|
83
|
-
const { obsEmbeddingIndex: obsIndex, theme, cellRadius = 1.0, cellOpacity = 1.0,
|
|
84
|
-
// cellFilter,
|
|
85
|
-
cellSelection, setCellHighlight, setComponentHover, getCellIsSelected, cellColors, getCellColor = makeDefaultGetCellColors(cellColors, obsIndex, theme), getExpressionValue, onCellClick, geneExpressionColormap, geneExpressionColormapRange = [0.0, 1.0], cellColorEncoding, } = this.props;
|
|
86
|
-
return new deck.ScatterplotLayer({
|
|
87
|
-
id: CELLS_LAYER_ID,
|
|
88
|
-
// Note that the reference for the object passed to the data prop should not change,
|
|
89
|
-
// otherwise DeckGL will need to do a full re-render every time .createCellsLayer is called,
|
|
90
|
-
// which can be very often to handle cellOpacity and cellRadius updates for dynamic opacity.
|
|
91
|
-
data: this.cellsData,
|
|
92
|
-
coordinateSystem: deck.COORDINATE_SYSTEM.CARTESIAN,
|
|
93
|
-
visible: true,
|
|
94
|
-
pickable: true,
|
|
95
|
-
autoHighlight: true,
|
|
96
|
-
filled: true,
|
|
97
|
-
stroked: true,
|
|
98
|
-
backgroundColor: (theme === 'dark' ? [0, 0, 0] : [241, 241, 241]),
|
|
99
|
-
getCellIsSelected,
|
|
100
|
-
opacity: cellOpacity,
|
|
101
|
-
radiusScale: cellRadius,
|
|
102
|
-
radiusMinPixels: 1,
|
|
103
|
-
radiusMaxPixels: 30,
|
|
104
|
-
// Our radius pixel setters measure in pixels.
|
|
105
|
-
radiusUnits: 'pixels',
|
|
106
|
-
lineWidthUnits: 'pixels',
|
|
107
|
-
getPosition,
|
|
108
|
-
getFillColor: getCellColor,
|
|
109
|
-
getLineColor: getCellColor,
|
|
110
|
-
getRadius: 1,
|
|
111
|
-
getExpressionValue,
|
|
112
|
-
getLineWidth: 0,
|
|
113
|
-
extensions: [
|
|
114
|
-
new ScaledExpressionExtension(),
|
|
115
|
-
new SelectionExtension({ instanced: true }),
|
|
116
|
-
],
|
|
117
|
-
colorScaleLo: geneExpressionColormapRange[0],
|
|
118
|
-
colorScaleHi: geneExpressionColormapRange[1],
|
|
119
|
-
isExpressionMode: (cellColorEncoding === 'geneSelection'),
|
|
120
|
-
colormap: geneExpressionColormap,
|
|
121
|
-
onClick: (info) => {
|
|
122
|
-
if (onCellClick) {
|
|
123
|
-
onCellClick(info);
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
onHover: getOnHoverCallback(obsIndex, setCellHighlight, setComponentHover),
|
|
127
|
-
updateTriggers: {
|
|
128
|
-
getExpressionValue,
|
|
129
|
-
getFillColor: [cellColorEncoding, cellSelection, cellColors],
|
|
130
|
-
getLineColor: [cellColorEncoding, cellSelection, cellColors],
|
|
131
|
-
getCellIsSelected,
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
createCellSetsLayers() {
|
|
136
|
-
const { theme, cellSetPolygons, viewState, cellSetPolygonsVisible, cellSetLabelsVisible, cellSetLabelSize, } = this.props;
|
|
137
|
-
const result = [];
|
|
138
|
-
if (cellSetPolygonsVisible) {
|
|
139
|
-
result.push(new deck.PolygonLayer({
|
|
140
|
-
id: 'cell-sets-polygon-layer',
|
|
141
|
-
data: cellSetPolygons,
|
|
142
|
-
stroked: true,
|
|
143
|
-
filled: false,
|
|
144
|
-
wireframe: true,
|
|
145
|
-
lineWidthMaxPixels: 1,
|
|
146
|
-
getPolygon: d => d.hull,
|
|
147
|
-
getLineColor: d => d.color,
|
|
148
|
-
getLineWidth: 1,
|
|
149
|
-
}));
|
|
150
|
-
}
|
|
151
|
-
if (cellSetLabelsVisible) {
|
|
152
|
-
const { zoom } = viewState;
|
|
153
|
-
const nodes = cellSetPolygons.map(p => ({
|
|
154
|
-
x: p.centroid[0],
|
|
155
|
-
y: p.centroid[1],
|
|
156
|
-
label: p.name,
|
|
157
|
-
}));
|
|
158
|
-
const collisionForce = this.cellSetsForceSimulation
|
|
159
|
-
.size(d => ([
|
|
160
|
-
cellSetLabelSize * 1 / (2 ** zoom) * 4 * d.label.length,
|
|
161
|
-
cellSetLabelSize * 1 / (2 ** zoom) * 1.5,
|
|
162
|
-
]));
|
|
163
|
-
forceSimulation()
|
|
164
|
-
.nodes(nodes)
|
|
165
|
-
.force('collision', collisionForce)
|
|
166
|
-
.tick(NUM_FORCE_SIMULATION_TICKS);
|
|
167
|
-
result.push(new deck.TextLayer({
|
|
168
|
-
id: 'cell-sets-text-layer',
|
|
169
|
-
data: nodes,
|
|
170
|
-
getPosition: d => ([d.x, d.y]),
|
|
171
|
-
getText: d => d.label,
|
|
172
|
-
getColor: (theme === 'dark' ? [255, 255, 255] : [0, 0, 0]),
|
|
173
|
-
getSize: cellSetLabelSize,
|
|
174
|
-
getAngle: 0,
|
|
175
|
-
getTextAnchor: 'middle',
|
|
176
|
-
getAlignmentBaseline: 'center',
|
|
177
|
-
fontFamily: LABEL_FONT_FAMILY,
|
|
178
|
-
fontWeight: 'normal',
|
|
179
|
-
}));
|
|
180
|
-
}
|
|
181
|
-
return result;
|
|
182
|
-
}
|
|
183
|
-
createSelectionLayers() {
|
|
184
|
-
const { obsEmbeddingIndex: obsIndex, obsEmbedding, viewState, setCellSelection, } = this.props;
|
|
185
|
-
const { tool } = this.state;
|
|
186
|
-
const { cellsQuadTree } = this;
|
|
187
|
-
const flipYTooltip = true;
|
|
188
|
-
const getCellCoords = makeDefaultGetObsCoords(obsEmbedding);
|
|
189
|
-
return getSelectionLayers(tool, viewState.zoom, CELLS_LAYER_ID, getCellCoords, obsIndex, setCellSelection, cellsQuadTree, flipYTooltip);
|
|
190
|
-
}
|
|
191
|
-
getLayers() {
|
|
192
|
-
const { cellsLayer, cellSetsLayers, } = this;
|
|
193
|
-
return [
|
|
194
|
-
cellsLayer,
|
|
195
|
-
...cellSetsLayers,
|
|
196
|
-
...this.createSelectionLayers(),
|
|
197
|
-
];
|
|
198
|
-
}
|
|
199
|
-
onUpdateCellsData() {
|
|
200
|
-
const { obsEmbedding } = this.props;
|
|
201
|
-
if (obsEmbedding) {
|
|
202
|
-
const getCellCoords = makeDefaultGetObsCoords(obsEmbedding);
|
|
203
|
-
this.cellsQuadTree = createQuadTree(obsEmbedding, getCellCoords);
|
|
204
|
-
this.cellsData = {
|
|
205
|
-
src: {
|
|
206
|
-
obsEmbedding,
|
|
207
|
-
},
|
|
208
|
-
length: obsEmbedding.shape[1],
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
onUpdateCellsLayer() {
|
|
213
|
-
const { obsEmbeddingIndex, obsEmbedding } = this.props;
|
|
214
|
-
if (obsEmbeddingIndex && obsEmbedding) {
|
|
215
|
-
this.cellsLayer = this.createCellsLayer();
|
|
216
|
-
}
|
|
217
|
-
else {
|
|
218
|
-
this.cellsLayer = null;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
onUpdateCellSetsLayers(onlyViewStateChange) {
|
|
222
|
-
// Because the label sizes for the force simulation depend on the zoom level,
|
|
223
|
-
// we _could_ run the simulation every time the zoom level changes.
|
|
224
|
-
// However, this has a performance impact in firefox.
|
|
225
|
-
if (onlyViewStateChange) {
|
|
226
|
-
const { viewState, cellSetLabelsVisible } = this.props;
|
|
227
|
-
const { zoom } = viewState;
|
|
228
|
-
const { cellSetsLabelPrevZoom } = this;
|
|
229
|
-
// Instead, we can just check if the zoom level has changed
|
|
230
|
-
// by some relatively large delta, to be more conservative
|
|
231
|
-
// about re-running the force simulation.
|
|
232
|
-
if (cellSetLabelsVisible
|
|
233
|
-
&& (cellSetsLabelPrevZoom === null
|
|
234
|
-
|| Math.abs(cellSetsLabelPrevZoom - zoom) > LABEL_UPDATE_ZOOM_DELTA)) {
|
|
235
|
-
this.cellSetsLayers = this.createCellSetsLayers();
|
|
236
|
-
this.cellSetsLabelPrevZoom = zoom;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
else {
|
|
240
|
-
// Otherwise, something more substantial than just
|
|
241
|
-
// the viewState has changed, such as the label array
|
|
242
|
-
// itself, so we always want to update the layer
|
|
243
|
-
// in this case.
|
|
244
|
-
this.cellSetsLayers = this.createCellSetsLayers();
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
viewInfoDidUpdate() {
|
|
248
|
-
const { obsEmbeddingIndex, obsEmbedding, } = this.props;
|
|
249
|
-
super.viewInfoDidUpdate(obsEmbeddingIndex, obsEmbedding, makeFlippedGetObsCoords);
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* Here, asynchronously check whether props have
|
|
253
|
-
* updated which require re-computing memoized variables,
|
|
254
|
-
* followed by a re-render.
|
|
255
|
-
* This function does not follow React conventions or paradigms,
|
|
256
|
-
* it is only implemented this way to try to squeeze out
|
|
257
|
-
* performance.
|
|
258
|
-
* @param {object} prevProps The previous props to diff against.
|
|
259
|
-
*/
|
|
260
|
-
componentDidUpdate(prevProps) {
|
|
261
|
-
this.viewInfoDidUpdate();
|
|
262
|
-
const shallowDiff = propName => (prevProps[propName] !== this.props[propName]);
|
|
263
|
-
let forceUpdate = false;
|
|
264
|
-
if (['obsEmbedding'].some(shallowDiff)) {
|
|
265
|
-
// Cells data changed.
|
|
266
|
-
this.onUpdateCellsData();
|
|
267
|
-
forceUpdate = true;
|
|
268
|
-
}
|
|
269
|
-
if ([
|
|
270
|
-
'obsEmbeddingIndex', 'obsEmbedding', 'cellFilter', 'cellSelection', 'cellColors',
|
|
271
|
-
'cellRadius', 'cellOpacity', 'cellRadiusMode', 'geneExpressionColormap',
|
|
272
|
-
'geneExpressionColormapRange', 'geneSelection', 'cellColorEncoding',
|
|
273
|
-
].some(shallowDiff)) {
|
|
274
|
-
// Cells layer props changed.
|
|
275
|
-
this.onUpdateCellsLayer();
|
|
276
|
-
forceUpdate = true;
|
|
277
|
-
}
|
|
278
|
-
if ([
|
|
279
|
-
'cellSetPolygons', 'cellSetPolygonsVisible',
|
|
280
|
-
'cellSetLabelsVisible', 'cellSetLabelSize',
|
|
281
|
-
].some(shallowDiff)) {
|
|
282
|
-
// Cell sets layer props changed.
|
|
283
|
-
this.onUpdateCellSetsLayers(false);
|
|
284
|
-
forceUpdate = true;
|
|
285
|
-
}
|
|
286
|
-
if (shallowDiff('viewState')) {
|
|
287
|
-
// The viewState prop has changed (due to zoom or pan).
|
|
288
|
-
this.onUpdateCellSetsLayers(true);
|
|
289
|
-
forceUpdate = true;
|
|
290
|
-
}
|
|
291
|
-
if (forceUpdate) {
|
|
292
|
-
this.forceUpdate();
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
/**
|
|
297
|
-
* Need this wrapper function here,
|
|
298
|
-
* since we want to pass a forwardRef
|
|
299
|
-
* so that outer components can
|
|
300
|
-
* access the grandchild DeckGL ref,
|
|
301
|
-
* but we are using a class component.
|
|
302
|
-
*/
|
|
303
|
-
const ScatterplotWrapper = forwardRef((props, deckRef) => (_jsx(Scatterplot, { ...props, deckRef: deckRef })));
|
|
304
|
-
export default ScatterplotWrapper;
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { useCallback } from 'react';
|
|
3
|
-
import debounce from 'lodash/debounce';
|
|
4
|
-
import Checkbox from '@material-ui/core/Checkbox';
|
|
5
|
-
import Slider from '@material-ui/core/Slider';
|
|
6
|
-
import TableCell from '@material-ui/core/TableCell';
|
|
7
|
-
import TableRow from '@material-ui/core/TableRow';
|
|
8
|
-
import { capitalize } from '@vitessce/utils';
|
|
9
|
-
import { usePlotOptionsStyles, CellColorEncodingOption, OptionsContainer, OptionSelect, } from '@vitessce/vit-s';
|
|
10
|
-
import { GLSL_COLORMAPS } from '@vitessce/gl';
|
|
11
|
-
export default function ScatterplotOptions(props) {
|
|
12
|
-
const { children, observationsLabel, cellRadius, setCellRadius, cellRadiusMode, setCellRadiusMode, cellOpacity, setCellOpacity, cellOpacityMode, setCellOpacityMode, cellSetLabelsVisible, setCellSetLabelsVisible, cellSetLabelSize, setCellSetLabelSize, cellSetPolygonsVisible, setCellSetPolygonsVisible, cellColorEncoding, setCellColorEncoding, geneExpressionColormap, setGeneExpressionColormap, geneExpressionColormapRange, setGeneExpressionColormapRange, } = props;
|
|
13
|
-
const observationsLabelNice = capitalize(observationsLabel);
|
|
14
|
-
const classes = usePlotOptionsStyles();
|
|
15
|
-
function handleCellRadiusModeChange(event) {
|
|
16
|
-
setCellRadiusMode(event.target.value);
|
|
17
|
-
}
|
|
18
|
-
function handleCellOpacityModeChange(event) {
|
|
19
|
-
setCellOpacityMode(event.target.value);
|
|
20
|
-
}
|
|
21
|
-
function handleRadiusChange(event, value) {
|
|
22
|
-
setCellRadius(value);
|
|
23
|
-
}
|
|
24
|
-
function handleOpacityChange(event, value) {
|
|
25
|
-
setCellOpacity(value);
|
|
26
|
-
}
|
|
27
|
-
function handleLabelVisibilityChange(event) {
|
|
28
|
-
setCellSetLabelsVisible(event.target.checked);
|
|
29
|
-
}
|
|
30
|
-
function handleLabelSizeChange(event, value) {
|
|
31
|
-
setCellSetLabelSize(value);
|
|
32
|
-
}
|
|
33
|
-
function handlePolygonVisibilityChange(event) {
|
|
34
|
-
setCellSetPolygonsVisible(event.target.checked);
|
|
35
|
-
}
|
|
36
|
-
function handleGeneExpressionColormapChange(event) {
|
|
37
|
-
setGeneExpressionColormap(event.target.value);
|
|
38
|
-
}
|
|
39
|
-
function handleColormapRangeChange(event, value) {
|
|
40
|
-
setGeneExpressionColormapRange(value);
|
|
41
|
-
}
|
|
42
|
-
const handleColormapRangeChangeDebounced = useCallback(debounce(handleColormapRangeChange, 5, { trailing: true }), [handleColormapRangeChange]);
|
|
43
|
-
return (_jsxs(OptionsContainer, { children: [children, _jsx(CellColorEncodingOption, { observationsLabel: observationsLabel, cellColorEncoding: cellColorEncoding, setCellColorEncoding: setCellColorEncoding }), _jsxs(TableRow, { children: [_jsxs(TableCell, { className: classes.labelCell, children: [observationsLabelNice, " Set Labels Visible"] }), _jsx(TableCell, { className: classes.inputCell, children: _jsx(Checkbox, { className: classes.checkbox, checked: cellSetLabelsVisible, onChange: handleLabelVisibilityChange, name: "scatterplot-option-cell-set-labels", color: "default" }) })] }), _jsxs(TableRow, { children: [_jsxs(TableCell, { className: classes.labelCell, children: [observationsLabelNice, " Set Label Size"] }), _jsx(TableCell, { className: classes.inputCell, children: _jsx(Slider, { disabled: !cellSetLabelsVisible, classes: { root: classes.slider, valueLabel: classes.sliderValueLabel }, value: cellSetLabelSize, onChange: handleLabelSizeChange, "aria-labelledby": "cell-set-label-size-slider", valueLabelDisplay: "auto", step: 1, min: 8, max: 36 }) })] }), _jsxs(TableRow, { children: [_jsxs(TableCell, { className: classes.labelCell, children: [observationsLabelNice, " Set Polygons Visible"] }), _jsx(TableCell, { className: classes.inputCell, children: _jsx(Checkbox, { className: classes.checkbox, checked: cellSetPolygonsVisible, onChange: handlePolygonVisibilityChange, name: "scatterplot-option-cell-set-polygons", color: "default" }) })] }), _jsxs(TableRow, { children: [_jsxs(TableCell, { className: classes.labelCell, htmlFor: "cell-radius-mode-select", children: [observationsLabelNice, " Radius Mode"] }), _jsx(TableCell, { className: classes.inputCell, children: _jsxs(OptionSelect, { className: classes.select, value: cellRadiusMode, onChange: handleCellRadiusModeChange, inputProps: {
|
|
44
|
-
id: 'cell-radius-mode-select',
|
|
45
|
-
}, children: [_jsx("option", { value: "auto", children: "Auto" }), _jsx("option", { value: "manual", children: "Manual" })] }) })] }), _jsxs(TableRow, { children: [_jsxs(TableCell, { className: classes.labelCell, children: [observationsLabelNice, " Radius"] }), _jsx(TableCell, { className: classes.inputCell, children: _jsx(Slider, { disabled: cellRadiusMode !== 'manual', classes: { root: classes.slider, valueLabel: classes.sliderValueLabel }, value: cellRadius, onChange: handleRadiusChange, "aria-labelledby": "cell-radius-slider", valueLabelDisplay: "auto", step: 0.01, min: 0.01, max: 10 }) })] }), _jsxs(TableRow, { children: [_jsxs(TableCell, { className: classes.labelCell, htmlFor: "cell-opacity-mode-select", children: [observationsLabelNice, " Opacity Mode"] }), _jsx(TableCell, { className: classes.inputCell, children: _jsxs(OptionSelect, { className: classes.select, value: cellOpacityMode, onChange: handleCellOpacityModeChange, inputProps: {
|
|
46
|
-
id: 'cell-opacity-mode-select',
|
|
47
|
-
}, children: [_jsx("option", { value: "auto", children: "Auto" }), _jsx("option", { value: "manual", children: "Manual" })] }) })] }), _jsxs(TableRow, { children: [_jsxs(TableCell, { className: classes.labelCell, children: [observationsLabelNice, " Opacity"] }), _jsx(TableCell, { className: classes.inputCell, children: _jsx(Slider, { disabled: cellOpacityMode !== 'manual', classes: { root: classes.slider, valueLabel: classes.sliderValueLabel }, value: cellOpacity, onChange: handleOpacityChange, "aria-labelledby": "cell-opacity-slider", valueLabelDisplay: "auto", step: 0.05, min: 0.0, max: 1.0 }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { className: classes.labelCell, htmlFor: "gene-expression-colormap-select", children: "Gene Expression Colormap" }), _jsx(TableCell, { className: classes.inputCell, children: _jsx(OptionSelect, { className: classes.select, value: geneExpressionColormap, onChange: handleGeneExpressionColormapChange, inputProps: {
|
|
48
|
-
id: 'gene-expression-colormap-select',
|
|
49
|
-
}, children: GLSL_COLORMAPS.map(cmap => (_jsx("option", { value: cmap, children: cmap }, cmap))) }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { className: classes.labelCell, children: "Gene Expression Colormap Range" }), _jsx(TableCell, { className: classes.inputCell, children: _jsx(Slider, { classes: { root: classes.slider, valueLabel: classes.sliderValueLabel }, value: geneExpressionColormapRange, onChange: handleColormapRangeChangeDebounced, "aria-labelledby": "gene-expression-colormap-range-slider", valueLabelDisplay: "auto", step: 0.005, min: 0.0, max: 1.0 }) })] })] }));
|
|
50
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { Tooltip2D, TooltipContent } from '@vitessce/tooltip';
|
|
4
|
-
import { useComponentHover, useComponentViewInfo } from '@vitessce/vit-s';
|
|
5
|
-
export default function ScatterplotTooltipSubscriber(props) {
|
|
6
|
-
const { parentUuid, obsHighlight, width, height, getObsInfo, } = props;
|
|
7
|
-
const sourceUuid = useComponentHover();
|
|
8
|
-
const viewInfo = useComponentViewInfo(parentUuid);
|
|
9
|
-
const [cellInfo, x, y] = (obsHighlight && getObsInfo ? ([
|
|
10
|
-
getObsInfo(obsHighlight),
|
|
11
|
-
...(viewInfo && viewInfo.project ? viewInfo.project(obsHighlight) : [null, null]),
|
|
12
|
-
]) : ([null, null, null]));
|
|
13
|
-
return ((cellInfo ? (_jsx(Tooltip2D, { x: x, y: y, parentUuid: parentUuid, sourceUuid: sourceUuid, parentWidth: width, parentHeight: height, children: _jsx(TooltipContent, { info: cellInfo }) })) : null));
|
|
14
|
-
}
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { PureComponent } from 'react';
|
|
3
|
-
import { deck, DEFAULT_GL_OPTIONS } from '@vitessce/gl';
|
|
4
|
-
import ToolMenu from './ToolMenu';
|
|
5
|
-
import { getCursor, getCursorWithTool } from './cursor';
|
|
6
|
-
/**
|
|
7
|
-
* Abstract class component intended to be inherited by
|
|
8
|
-
* the Spatial and Scatterplot class components.
|
|
9
|
-
* Contains a common constructor, common DeckGL callbacks,
|
|
10
|
-
* and common render function.
|
|
11
|
-
*/
|
|
12
|
-
export default class AbstractSpatialOrScatterplot extends PureComponent {
|
|
13
|
-
constructor(props) {
|
|
14
|
-
super(props);
|
|
15
|
-
this.state = {
|
|
16
|
-
gl: null,
|
|
17
|
-
tool: null,
|
|
18
|
-
};
|
|
19
|
-
this.viewport = null;
|
|
20
|
-
this.onViewStateChange = this.onViewStateChange.bind(this);
|
|
21
|
-
this.onInitializeViewInfo = this.onInitializeViewInfo.bind(this);
|
|
22
|
-
this.onWebGLInitialized = this.onWebGLInitialized.bind(this);
|
|
23
|
-
this.onToolChange = this.onToolChange.bind(this);
|
|
24
|
-
this.onHover = this.onHover.bind(this);
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Called by DeckGL upon a viewState change,
|
|
28
|
-
* for example zoom or pan interaction.
|
|
29
|
-
* Emit the new viewState to the `setViewState`
|
|
30
|
-
* handler prop.
|
|
31
|
-
* @param {object} params
|
|
32
|
-
* @param {object} params.viewState The next deck.gl viewState.
|
|
33
|
-
*/
|
|
34
|
-
onViewStateChange({ viewState: nextViewState }) {
|
|
35
|
-
const { setViewState, viewState, layers, spatialAxisFixed, } = this.props;
|
|
36
|
-
const use3d = layers?.some(l => l.use3d);
|
|
37
|
-
setViewState({
|
|
38
|
-
...nextViewState,
|
|
39
|
-
// If the axis is fixed, just use the current target in state i.e don't change target.
|
|
40
|
-
target: spatialAxisFixed && use3d ? viewState.target : nextViewState.target,
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Called by DeckGL upon viewport
|
|
45
|
-
* initialization.
|
|
46
|
-
* @param {object} viewState
|
|
47
|
-
* @param {object} viewState.viewport
|
|
48
|
-
*/
|
|
49
|
-
onInitializeViewInfo({ viewport }) {
|
|
50
|
-
this.viewport = viewport;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Called by DeckGL upon initialization,
|
|
54
|
-
* helps to understand when to pass layers
|
|
55
|
-
* to the DeckGL component.
|
|
56
|
-
* @param {object} gl The WebGL context object.
|
|
57
|
-
*/
|
|
58
|
-
onWebGLInitialized(gl) {
|
|
59
|
-
this.setState({ gl });
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Called by the ToolMenu buttons.
|
|
63
|
-
* Emits the new tool value to the
|
|
64
|
-
* `onToolChange` prop.
|
|
65
|
-
* @param {string} tool Name of tool.
|
|
66
|
-
*/
|
|
67
|
-
onToolChange(tool) {
|
|
68
|
-
const { onToolChange: onToolChangeProp } = this.props;
|
|
69
|
-
this.setState({ tool });
|
|
70
|
-
if (onToolChangeProp) {
|
|
71
|
-
onToolChangeProp(tool);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Create the DeckGL layers.
|
|
76
|
-
* @returns {object[]} Array of
|
|
77
|
-
* DeckGL layer objects.
|
|
78
|
-
* Intended to be overriden by descendants.
|
|
79
|
-
*/
|
|
80
|
-
// eslint-disable-next-line class-methods-use-this
|
|
81
|
-
getLayers() {
|
|
82
|
-
return [];
|
|
83
|
-
}
|
|
84
|
-
// eslint-disable-next-line consistent-return
|
|
85
|
-
onHover(info) {
|
|
86
|
-
const { coordinate, sourceLayer: layer, tile, } = info;
|
|
87
|
-
const { setCellHighlight, cellHighlight, setComponentHover, layers, } = this.props;
|
|
88
|
-
const hasBitmask = (layers || []).some(l => l.type === 'bitmask');
|
|
89
|
-
if (!setCellHighlight || !tile) {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
if (!layer || !coordinate) {
|
|
93
|
-
if (cellHighlight && hasBitmask) {
|
|
94
|
-
setCellHighlight(null);
|
|
95
|
-
}
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
const { content, bbox, index: { z }, } = tile;
|
|
99
|
-
if (!content) {
|
|
100
|
-
if (cellHighlight && hasBitmask) {
|
|
101
|
-
setCellHighlight(null);
|
|
102
|
-
}
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
const { data, width, height } = content;
|
|
106
|
-
const { left, right, top, bottom, } = bbox;
|
|
107
|
-
const bounds = [
|
|
108
|
-
left,
|
|
109
|
-
data.height < layer.tileSize ? height : bottom,
|
|
110
|
-
data.width < layer.tileSize ? width : right,
|
|
111
|
-
top,
|
|
112
|
-
];
|
|
113
|
-
if (!data) {
|
|
114
|
-
if (cellHighlight && hasBitmask) {
|
|
115
|
-
setCellHighlight(null);
|
|
116
|
-
}
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
// Tiled layer needs a custom layerZoomScale.
|
|
120
|
-
if (layer.id.includes('bitmask')) {
|
|
121
|
-
// The zoomed out layer needs to use the fixed zoom at which it is rendered.
|
|
122
|
-
const layerZoomScale = Math.max(1, 2 ** Math.round(-z));
|
|
123
|
-
const dataCoords = [
|
|
124
|
-
Math.floor((coordinate[0] - bounds[0]) / layerZoomScale),
|
|
125
|
-
Math.floor((coordinate[1] - bounds[3]) / layerZoomScale),
|
|
126
|
-
];
|
|
127
|
-
const coords = dataCoords[1] * width + dataCoords[0];
|
|
128
|
-
const hoverData = data.map(d => d[coords]);
|
|
129
|
-
const cellId = hoverData.find(i => i > 0);
|
|
130
|
-
if (cellId !== Number(cellHighlight)) {
|
|
131
|
-
if (setComponentHover) {
|
|
132
|
-
setComponentHover();
|
|
133
|
-
}
|
|
134
|
-
// eslint-disable-next-line no-unused-expressions
|
|
135
|
-
setCellHighlight(cellId ? String(cellId) : null);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Emits a function to project from the
|
|
141
|
-
* cell ID space to the scatterplot or
|
|
142
|
-
* spatial coordinate space, via the
|
|
143
|
-
* `updateViewInfo` prop.
|
|
144
|
-
*/
|
|
145
|
-
viewInfoDidUpdate(obsIndex, obsLocations, makeGetObsCoords) {
|
|
146
|
-
const { updateViewInfo, uuid } = this.props;
|
|
147
|
-
const { viewport } = this;
|
|
148
|
-
if (updateViewInfo && viewport) {
|
|
149
|
-
updateViewInfo({
|
|
150
|
-
uuid,
|
|
151
|
-
project: (obsId) => {
|
|
152
|
-
try {
|
|
153
|
-
if (obsIndex && obsLocations) {
|
|
154
|
-
const getObsCoords = makeGetObsCoords(obsLocations);
|
|
155
|
-
const obsIdx = obsIndex.indexOf(obsId);
|
|
156
|
-
const obsCoord = getObsCoords(obsIdx);
|
|
157
|
-
return viewport.project(obsCoord);
|
|
158
|
-
}
|
|
159
|
-
return [null, null];
|
|
160
|
-
}
|
|
161
|
-
catch (e) {
|
|
162
|
-
return [null, null];
|
|
163
|
-
}
|
|
164
|
-
},
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* Intended to be overridden by descendants.
|
|
170
|
-
*/
|
|
171
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
172
|
-
componentDidUpdate() {
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Intended to be overridden by descendants.
|
|
176
|
-
* @returns {boolean} Whether or not any layers are 3D.
|
|
177
|
-
*/
|
|
178
|
-
// eslint-disable-next-line class-methods-use-this
|
|
179
|
-
use3d() {
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* A common render function for both Spatial
|
|
184
|
-
* and Scatterplot components.
|
|
185
|
-
*/
|
|
186
|
-
render() {
|
|
187
|
-
const { deckRef, viewState, uuid, hideTools, } = this.props;
|
|
188
|
-
const { gl, tool } = this.state;
|
|
189
|
-
const layers = this.getLayers();
|
|
190
|
-
const use3d = this.use3d();
|
|
191
|
-
const showCellSelectionTools = this.obsSegmentationsData !== null;
|
|
192
|
-
const showPanTool = layers.length > 0;
|
|
193
|
-
// For large datasets or ray casting, the visual quality takes only a small
|
|
194
|
-
// hit in exchange for much better performance by setting this to false:
|
|
195
|
-
// https://deck.gl/docs/api-reference/core/deck#usedevicepixels
|
|
196
|
-
const useDevicePixels = (!use3d
|
|
197
|
-
&& (this.obsSegmentationsData?.shape?.[0] < 100000
|
|
198
|
-
|| this.obsLocationsData?.shape?.[1] < 100000));
|
|
199
|
-
return (_jsxs(_Fragment, { children: [_jsx(ToolMenu, { activeTool: tool, setActiveTool: this.onToolChange, visibleTools: {
|
|
200
|
-
pan: showPanTool && !hideTools,
|
|
201
|
-
selectRectangle: showCellSelectionTools && !hideTools,
|
|
202
|
-
selectLasso: showCellSelectionTools && !hideTools,
|
|
203
|
-
} }), _jsx(deck.DeckGL, { id: `deckgl-overlay-${uuid}`, ref: deckRef, views: [
|
|
204
|
-
use3d
|
|
205
|
-
? new deck.OrbitView({ id: 'orbit', controller: true, orbitAxis: 'Y' })
|
|
206
|
-
: new deck.OrthographicView({
|
|
207
|
-
id: 'ortho',
|
|
208
|
-
}),
|
|
209
|
-
], layers: gl && viewState.target.slice(0, 2).every(i => typeof i === 'number')
|
|
210
|
-
? layers
|
|
211
|
-
: [], glOptions: DEFAULT_GL_OPTIONS, onWebGLInitialized: this.onWebGLInitialized, onViewStateChange: this.onViewStateChange, viewState: viewState, useDevicePixels: useDevicePixels, controller: tool ? { dragPan: false } : true, getCursor: tool ? getCursorWithTool : getCursor, onHover: this.onHover, children: this.onInitializeViewInfo })] }));
|
|
212
|
-
}
|
|
213
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import clsx from 'clsx';
|
|
4
|
-
import { SELECTION_TYPE } from '@vitessce/gl';
|
|
5
|
-
import { PointerIconSVG, SelectRectangleIconSVG, SelectLassoIconSVG } from '@vitessce/icons';
|
|
6
|
-
import { makeStyles } from '@material-ui/core';
|
|
7
|
-
const useStyles = makeStyles(() => ({
|
|
8
|
-
tool: {
|
|
9
|
-
position: 'absolute',
|
|
10
|
-
display: 'inline',
|
|
11
|
-
zIndex: '1000',
|
|
12
|
-
opacity: '.65',
|
|
13
|
-
color: 'black',
|
|
14
|
-
'&:hover': {
|
|
15
|
-
opacity: '.90',
|
|
16
|
-
},
|
|
17
|
-
},
|
|
18
|
-
iconButton: {
|
|
19
|
-
// btn btn-outline-secondary mr-2 icon
|
|
20
|
-
padding: '0',
|
|
21
|
-
height: '2em',
|
|
22
|
-
width: '2em',
|
|
23
|
-
backgroundColor: 'white',
|
|
24
|
-
display: 'inline-block',
|
|
25
|
-
fontWeight: '400',
|
|
26
|
-
textAlign: 'center',
|
|
27
|
-
verticalAlign: 'middle',
|
|
28
|
-
cursor: 'pointer',
|
|
29
|
-
userSelect: 'none',
|
|
30
|
-
border: '1px solid #6c757d',
|
|
31
|
-
fontSize: '16px',
|
|
32
|
-
lineHeight: '1.5',
|
|
33
|
-
borderRadius: '4px',
|
|
34
|
-
transition: 'color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out',
|
|
35
|
-
color: '#6c757d',
|
|
36
|
-
marginRight: '8px',
|
|
37
|
-
'& > svg': {
|
|
38
|
-
verticalAlign: 'middle',
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
iconButtonActive: {
|
|
42
|
-
// active
|
|
43
|
-
color: '#fff',
|
|
44
|
-
backgroundColor: '#6c757d',
|
|
45
|
-
borderColor: '#6c757d',
|
|
46
|
-
boxShadow: '0 0 0 3px rgba(108, 117, 125, 0.5)',
|
|
47
|
-
},
|
|
48
|
-
}));
|
|
49
|
-
export function IconButton(props) {
|
|
50
|
-
const { alt, onClick, isActive, children, } = props;
|
|
51
|
-
const classes = useStyles();
|
|
52
|
-
return (_jsx("button", { className: clsx(classes.iconButton, { [classes.iconButtonActive]: isActive }), onClick: onClick, type: "button", title: alt, children: children }));
|
|
53
|
-
}
|
|
54
|
-
export default function ToolMenu(props) {
|
|
55
|
-
const { setActiveTool, activeTool, visibleTools = { pan: true, selectRectangle: true, selectLasso: true }, } = props;
|
|
56
|
-
const classes = useStyles();
|
|
57
|
-
return (_jsxs("div", { className: classes.tool, children: [visibleTools.pan && (_jsx(IconButton, { alt: "pointer tool", onClick: () => setActiveTool(null), isActive: activeTool === null, children: _jsx(PointerIconSVG, {}) })), visibleTools.selectRectangle ? (_jsx(IconButton, { alt: "select rectangle", onClick: () => setActiveTool(SELECTION_TYPE.RECTANGLE), isActive: activeTool === SELECTION_TYPE.RECTANGLE, children: _jsx(SelectRectangleIconSVG, {}) })) : null, visibleTools.selectLasso ? (_jsx(IconButton, { alt: "select lasso", onClick: () => setActiveTool(SELECTION_TYPE.POLYGON), isActive: activeTool === SELECTION_TYPE.POLYGON, children: _jsx(SelectLassoIconSVG, {}) })) : null] }));
|
|
58
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import '@testing-library/jest-dom';
|
|
3
|
-
import { cleanup, render } from '@testing-library/react';
|
|
4
|
-
import { afterEach, expect } from 'vitest';
|
|
5
|
-
import { IconButton } from './ToolMenu';
|
|
6
|
-
afterEach(() => {
|
|
7
|
-
cleanup();
|
|
8
|
-
});
|
|
9
|
-
describe('ToolMenu.js', () => {
|
|
10
|
-
describe('<IconButton />', () => {
|
|
11
|
-
it('renders with title attribute', () => {
|
|
12
|
-
const { container } = render(_jsx(IconButton, { isActive: true, alt: "Lasso" }));
|
|
13
|
-
expect(container.querySelectorAll('[title="Lasso"]').length).toEqual(1);
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
});
|