@vitessce/scatterplot 2.0.2 → 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.
Files changed (40) hide show
  1. package/dist/deflate.65a17097.mjs +13 -0
  2. package/dist/index.8206952d.mjs +132343 -0
  3. package/dist/index.mjs +15 -0
  4. package/dist/jpeg.4221d32f.mjs +840 -0
  5. package/dist/lerc.8d649494.mjs +1943 -0
  6. package/dist/lzw.89350f4e.mjs +128 -0
  7. package/dist/packbits.986f9d9f.mjs +30 -0
  8. package/dist/pako.esm.4b234125.mjs +3940 -0
  9. package/dist/raw.1cc73933.mjs +12 -0
  10. package/dist/webimage.be69a2d5.mjs +32 -0
  11. package/{dist → dist-tsc}/index.js +0 -0
  12. package/package.json +11 -11
  13. package/src/EmptyMessage.js +11 -0
  14. package/src/Scatterplot.js +385 -0
  15. package/src/ScatterplotOptions.js +247 -0
  16. package/src/ScatterplotTooltipSubscriber.js +38 -0
  17. package/src/index.js +11 -0
  18. package/src/shared-spatial-scatterplot/AbstractSpatialOrScatterplot.js +274 -0
  19. package/src/shared-spatial-scatterplot/ToolMenu.js +105 -0
  20. package/src/shared-spatial-scatterplot/ToolMenu.test.jsx +18 -0
  21. package/src/shared-spatial-scatterplot/cursor.js +23 -0
  22. package/src/shared-spatial-scatterplot/dynamic-opacity.js +58 -0
  23. package/src/shared-spatial-scatterplot/dynamic-opacity.test.js +33 -0
  24. package/src/shared-spatial-scatterplot/force-collide-rects.js +189 -0
  25. package/src/shared-spatial-scatterplot/force-collide-rects.test.js +72 -0
  26. package/{dist → src}/shared-spatial-scatterplot/index.js +4 -1
  27. package/src/shared-spatial-scatterplot/quadtree.js +27 -0
  28. package/dist/EmptyMessage.js +0 -6
  29. package/dist/Scatterplot.js +0 -304
  30. package/dist/ScatterplotOptions.js +0 -50
  31. package/dist/ScatterplotTooltipSubscriber.js +0 -14
  32. package/dist/shared-spatial-scatterplot/AbstractSpatialOrScatterplot.js +0 -213
  33. package/dist/shared-spatial-scatterplot/ToolMenu.js +0 -58
  34. package/dist/shared-spatial-scatterplot/ToolMenu.test.js +0 -16
  35. package/dist/shared-spatial-scatterplot/cursor.js +0 -22
  36. package/dist/shared-spatial-scatterplot/dynamic-opacity.js +0 -47
  37. package/dist/shared-spatial-scatterplot/dynamic-opacity.test.js +0 -28
  38. package/dist/shared-spatial-scatterplot/force-collide-rects.js +0 -169
  39. package/dist/shared-spatial-scatterplot/force-collide-rects.test.js +0 -58
  40. package/dist/shared-spatial-scatterplot/quadtree.js +0 -26
@@ -0,0 +1,247 @@
1
+ import React, { useCallback } from 'react';
2
+ import debounce from 'lodash/debounce';
3
+ import Checkbox from '@material-ui/core/Checkbox';
4
+ import Slider from '@material-ui/core/Slider';
5
+ import TableCell from '@material-ui/core/TableCell';
6
+ import TableRow from '@material-ui/core/TableRow';
7
+ import { capitalize } from '@vitessce/utils';
8
+ import {
9
+ usePlotOptionsStyles, CellColorEncodingOption, OptionsContainer, OptionSelect,
10
+ } from '@vitessce/vit-s';
11
+ import { GLSL_COLORMAPS } from '@vitessce/gl';
12
+
13
+ export default function ScatterplotOptions(props) {
14
+ const {
15
+ children,
16
+ observationsLabel,
17
+ cellRadius,
18
+ setCellRadius,
19
+ cellRadiusMode,
20
+ setCellRadiusMode,
21
+ cellOpacity,
22
+ setCellOpacity,
23
+ cellOpacityMode,
24
+ setCellOpacityMode,
25
+ cellSetLabelsVisible,
26
+ setCellSetLabelsVisible,
27
+ cellSetLabelSize,
28
+ setCellSetLabelSize,
29
+ cellSetPolygonsVisible,
30
+ setCellSetPolygonsVisible,
31
+ cellColorEncoding,
32
+ setCellColorEncoding,
33
+ geneExpressionColormap,
34
+ setGeneExpressionColormap,
35
+ geneExpressionColormapRange,
36
+ setGeneExpressionColormapRange,
37
+ } = props;
38
+
39
+ const observationsLabelNice = capitalize(observationsLabel);
40
+
41
+ const classes = usePlotOptionsStyles();
42
+
43
+ function handleCellRadiusModeChange(event) {
44
+ setCellRadiusMode(event.target.value);
45
+ }
46
+
47
+ function handleCellOpacityModeChange(event) {
48
+ setCellOpacityMode(event.target.value);
49
+ }
50
+
51
+ function handleRadiusChange(event, value) {
52
+ setCellRadius(value);
53
+ }
54
+
55
+ function handleOpacityChange(event, value) {
56
+ setCellOpacity(value);
57
+ }
58
+
59
+ function handleLabelVisibilityChange(event) {
60
+ setCellSetLabelsVisible(event.target.checked);
61
+ }
62
+
63
+ function handleLabelSizeChange(event, value) {
64
+ setCellSetLabelSize(value);
65
+ }
66
+
67
+ function handlePolygonVisibilityChange(event) {
68
+ setCellSetPolygonsVisible(event.target.checked);
69
+ }
70
+
71
+ function handleGeneExpressionColormapChange(event) {
72
+ setGeneExpressionColormap(event.target.value);
73
+ }
74
+
75
+ function handleColormapRangeChange(event, value) {
76
+ setGeneExpressionColormapRange(value);
77
+ }
78
+ const handleColormapRangeChangeDebounced = useCallback(
79
+ debounce(handleColormapRangeChange, 5, { trailing: true }),
80
+ [handleColormapRangeChange],
81
+ );
82
+
83
+ return (
84
+ <OptionsContainer>
85
+ {children}
86
+ <CellColorEncodingOption
87
+ observationsLabel={observationsLabel}
88
+ cellColorEncoding={cellColorEncoding}
89
+ setCellColorEncoding={setCellColorEncoding}
90
+ />
91
+ <TableRow>
92
+ <TableCell className={classes.labelCell}>
93
+ {observationsLabelNice} Set Labels Visible
94
+ </TableCell>
95
+ <TableCell className={classes.inputCell}>
96
+ <Checkbox
97
+ className={classes.checkbox}
98
+ checked={cellSetLabelsVisible}
99
+ onChange={handleLabelVisibilityChange}
100
+ name="scatterplot-option-cell-set-labels"
101
+ color="default"
102
+ />
103
+ </TableCell>
104
+ </TableRow>
105
+ <TableRow>
106
+ <TableCell className={classes.labelCell}>
107
+ {observationsLabelNice} Set Label Size
108
+ </TableCell>
109
+ <TableCell className={classes.inputCell}>
110
+ <Slider
111
+ disabled={!cellSetLabelsVisible}
112
+ classes={{ root: classes.slider, valueLabel: classes.sliderValueLabel }}
113
+ value={cellSetLabelSize}
114
+ onChange={handleLabelSizeChange}
115
+ aria-labelledby="cell-set-label-size-slider"
116
+ valueLabelDisplay="auto"
117
+ step={1}
118
+ min={8}
119
+ max={36}
120
+ />
121
+ </TableCell>
122
+ </TableRow>
123
+ <TableRow>
124
+ <TableCell className={classes.labelCell}>
125
+ {observationsLabelNice} Set Polygons Visible
126
+ </TableCell>
127
+ <TableCell className={classes.inputCell}>
128
+ <Checkbox
129
+ className={classes.checkbox}
130
+ checked={cellSetPolygonsVisible}
131
+ onChange={handlePolygonVisibilityChange}
132
+ name="scatterplot-option-cell-set-polygons"
133
+ color="default"
134
+ />
135
+ </TableCell>
136
+ </TableRow>
137
+ <TableRow>
138
+ <TableCell className={classes.labelCell} htmlFor="cell-radius-mode-select">
139
+ {observationsLabelNice} Radius Mode
140
+ </TableCell>
141
+ <TableCell className={classes.inputCell}>
142
+ <OptionSelect
143
+ className={classes.select}
144
+ value={cellRadiusMode}
145
+ onChange={handleCellRadiusModeChange}
146
+ inputProps={{
147
+ id: 'cell-radius-mode-select',
148
+ }}
149
+ >
150
+ <option value="auto">Auto</option>
151
+ <option value="manual">Manual</option>
152
+ </OptionSelect>
153
+ </TableCell>
154
+ </TableRow>
155
+ <TableRow>
156
+ <TableCell className={classes.labelCell}>
157
+ {observationsLabelNice} Radius
158
+ </TableCell>
159
+ <TableCell className={classes.inputCell}>
160
+ <Slider
161
+ disabled={cellRadiusMode !== 'manual'}
162
+ classes={{ root: classes.slider, valueLabel: classes.sliderValueLabel }}
163
+ value={cellRadius}
164
+ onChange={handleRadiusChange}
165
+ aria-labelledby="cell-radius-slider"
166
+ valueLabelDisplay="auto"
167
+ step={0.01}
168
+ min={0.01}
169
+ max={10}
170
+ />
171
+ </TableCell>
172
+ </TableRow>
173
+ <TableRow>
174
+ <TableCell className={classes.labelCell} htmlFor="cell-opacity-mode-select">
175
+ {observationsLabelNice} Opacity Mode
176
+ </TableCell>
177
+ <TableCell className={classes.inputCell}>
178
+ <OptionSelect
179
+ className={classes.select}
180
+ value={cellOpacityMode}
181
+ onChange={handleCellOpacityModeChange}
182
+ inputProps={{
183
+ id: 'cell-opacity-mode-select',
184
+ }}
185
+ >
186
+ <option value="auto">Auto</option>
187
+ <option value="manual">Manual</option>
188
+ </OptionSelect>
189
+ </TableCell>
190
+ </TableRow>
191
+ <TableRow>
192
+ <TableCell className={classes.labelCell}>
193
+ {observationsLabelNice} Opacity
194
+ </TableCell>
195
+ <TableCell className={classes.inputCell}>
196
+ <Slider
197
+ disabled={cellOpacityMode !== 'manual'}
198
+ classes={{ root: classes.slider, valueLabel: classes.sliderValueLabel }}
199
+ value={cellOpacity}
200
+ onChange={handleOpacityChange}
201
+ aria-labelledby="cell-opacity-slider"
202
+ valueLabelDisplay="auto"
203
+ step={0.05}
204
+ min={0.0}
205
+ max={1.0}
206
+ />
207
+ </TableCell>
208
+ </TableRow>
209
+ <TableRow>
210
+ <TableCell className={classes.labelCell} htmlFor="gene-expression-colormap-select">
211
+ Gene Expression Colormap
212
+ </TableCell>
213
+ <TableCell className={classes.inputCell}>
214
+ <OptionSelect
215
+ className={classes.select}
216
+ value={geneExpressionColormap}
217
+ onChange={handleGeneExpressionColormapChange}
218
+ inputProps={{
219
+ id: 'gene-expression-colormap-select',
220
+ }}
221
+ >
222
+ {GLSL_COLORMAPS.map(cmap => (
223
+ <option key={cmap} value={cmap}>{cmap}</option>
224
+ ))}
225
+ </OptionSelect>
226
+ </TableCell>
227
+ </TableRow>
228
+ <TableRow>
229
+ <TableCell className={classes.labelCell}>
230
+ Gene Expression Colormap Range
231
+ </TableCell>
232
+ <TableCell className={classes.inputCell}>
233
+ <Slider
234
+ classes={{ root: classes.slider, valueLabel: classes.sliderValueLabel }}
235
+ value={geneExpressionColormapRange}
236
+ onChange={handleColormapRangeChangeDebounced}
237
+ aria-labelledby="gene-expression-colormap-range-slider"
238
+ valueLabelDisplay="auto"
239
+ step={0.005}
240
+ min={0.0}
241
+ max={1.0}
242
+ />
243
+ </TableCell>
244
+ </TableRow>
245
+ </OptionsContainer>
246
+ );
247
+ }
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import { Tooltip2D, TooltipContent } from '@vitessce/tooltip';
3
+ import { useComponentHover, useComponentViewInfo } from '@vitessce/vit-s';
4
+
5
+ export default function ScatterplotTooltipSubscriber(props) {
6
+ const {
7
+ parentUuid,
8
+ obsHighlight,
9
+ width,
10
+ height,
11
+ getObsInfo,
12
+ } = props;
13
+
14
+ const sourceUuid = useComponentHover();
15
+ const viewInfo = useComponentViewInfo(parentUuid);
16
+
17
+ const [cellInfo, x, y] = (obsHighlight && getObsInfo ? (
18
+ [
19
+ getObsInfo(obsHighlight),
20
+ ...(viewInfo && viewInfo.project ? viewInfo.project(obsHighlight) : [null, null]),
21
+ ]
22
+ ) : ([null, null, null]));
23
+
24
+ return (
25
+ (cellInfo ? (
26
+ <Tooltip2D
27
+ x={x}
28
+ y={y}
29
+ parentUuid={parentUuid}
30
+ sourceUuid={sourceUuid}
31
+ parentWidth={width}
32
+ parentHeight={height}
33
+ >
34
+ <TooltipContent info={cellInfo} />
35
+ </Tooltip2D>
36
+ ) : null)
37
+ );
38
+ }
package/src/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export { default as Scatterplot } from './Scatterplot';
2
+ export { default as ScatterplotOptions } from './ScatterplotOptions';
3
+ export { default as ScatterplotTooltipSubscriber } from './ScatterplotTooltipSubscriber';
4
+ export { default as EmptyMessage } from './EmptyMessage';
5
+ export {
6
+ getPointSizeDevicePixels,
7
+ getPointOpacity,
8
+ getOnHoverCallback,
9
+ createQuadTree,
10
+ AbstractSpatialOrScatterplot,
11
+ } from './shared-spatial-scatterplot/index';
@@ -0,0 +1,274 @@
1
+ import React, { PureComponent } from 'react';
2
+ import { deck, DEFAULT_GL_OPTIONS } from '@vitessce/gl';
3
+ import ToolMenu from './ToolMenu';
4
+ import { getCursor, getCursorWithTool } from './cursor';
5
+
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
+
16
+ this.state = {
17
+ gl: null,
18
+ tool: null,
19
+ };
20
+
21
+ this.viewport = null;
22
+
23
+ this.onViewStateChange = this.onViewStateChange.bind(this);
24
+ this.onInitializeViewInfo = this.onInitializeViewInfo.bind(this);
25
+ this.onWebGLInitialized = this.onWebGLInitialized.bind(this);
26
+ this.onToolChange = this.onToolChange.bind(this);
27
+ this.onHover = this.onHover.bind(this);
28
+ }
29
+
30
+ /**
31
+ * Called by DeckGL upon a viewState change,
32
+ * for example zoom or pan interaction.
33
+ * Emit the new viewState to the `setViewState`
34
+ * handler prop.
35
+ * @param {object} params
36
+ * @param {object} params.viewState The next deck.gl viewState.
37
+ */
38
+ onViewStateChange({ viewState: nextViewState }) {
39
+ const {
40
+ setViewState, viewState, layers, spatialAxisFixed,
41
+ } = this.props;
42
+ const use3d = layers?.some(l => l.use3d);
43
+ setViewState({
44
+ ...nextViewState,
45
+ // If the axis is fixed, just use the current target in state i.e don't change target.
46
+ target: spatialAxisFixed && use3d ? viewState.target : nextViewState.target,
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Called by DeckGL upon viewport
52
+ * initialization.
53
+ * @param {object} viewState
54
+ * @param {object} viewState.viewport
55
+ */
56
+ onInitializeViewInfo({ viewport }) {
57
+ this.viewport = viewport;
58
+ }
59
+
60
+ /**
61
+ * Called by DeckGL upon initialization,
62
+ * helps to understand when to pass layers
63
+ * to the DeckGL component.
64
+ * @param {object} gl The WebGL context object.
65
+ */
66
+ onWebGLInitialized(gl) {
67
+ this.setState({ gl });
68
+ }
69
+
70
+ /**
71
+ * Called by the ToolMenu buttons.
72
+ * Emits the new tool value to the
73
+ * `onToolChange` prop.
74
+ * @param {string} tool Name of tool.
75
+ */
76
+ onToolChange(tool) {
77
+ const { onToolChange: onToolChangeProp } = this.props;
78
+ this.setState({ tool });
79
+ if (onToolChangeProp) {
80
+ onToolChangeProp(tool);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Create the DeckGL layers.
86
+ * @returns {object[]} Array of
87
+ * DeckGL layer objects.
88
+ * Intended to be overriden by descendants.
89
+ */
90
+ // eslint-disable-next-line class-methods-use-this
91
+ getLayers() {
92
+ return [];
93
+ }
94
+
95
+ // eslint-disable-next-line consistent-return
96
+ onHover(info) {
97
+ const {
98
+ coordinate, sourceLayer: layer, tile,
99
+ } = info;
100
+ const {
101
+ setCellHighlight, cellHighlight, setComponentHover, layers,
102
+ } = this.props;
103
+ const hasBitmask = (layers || []).some(l => l.type === 'bitmask');
104
+ if (!setCellHighlight || !tile) {
105
+ return null;
106
+ }
107
+ if (!layer || !coordinate) {
108
+ if (cellHighlight && hasBitmask) {
109
+ setCellHighlight(null);
110
+ }
111
+ return null;
112
+ }
113
+ const {
114
+ content,
115
+ bbox,
116
+ index: { z },
117
+ } = tile;
118
+ if (!content) {
119
+ if (cellHighlight && hasBitmask) {
120
+ setCellHighlight(null);
121
+ }
122
+ return null;
123
+ }
124
+ const { data, width, height } = content;
125
+ const {
126
+ left, right, top, bottom,
127
+ } = bbox;
128
+ const bounds = [
129
+ left,
130
+ data.height < layer.tileSize ? height : bottom,
131
+ data.width < layer.tileSize ? width : right,
132
+ top,
133
+ ];
134
+ if (!data) {
135
+ if (cellHighlight && hasBitmask) {
136
+ setCellHighlight(null);
137
+ }
138
+ return null;
139
+ }
140
+ // Tiled layer needs a custom layerZoomScale.
141
+ if (layer.id.includes('bitmask')) {
142
+ // The zoomed out layer needs to use the fixed zoom at which it is rendered.
143
+ const layerZoomScale = Math.max(
144
+ 1,
145
+ 2 ** Math.round(-z),
146
+ );
147
+ const dataCoords = [
148
+ Math.floor((coordinate[0] - bounds[0]) / layerZoomScale),
149
+ Math.floor((coordinate[1] - bounds[3]) / layerZoomScale),
150
+ ];
151
+ const coords = dataCoords[1] * width + dataCoords[0];
152
+ const hoverData = data.map(d => d[coords]);
153
+ const cellId = hoverData.find(i => i > 0);
154
+ if (cellId !== Number(cellHighlight)) {
155
+ if (setComponentHover) {
156
+ setComponentHover();
157
+ }
158
+ // eslint-disable-next-line no-unused-expressions
159
+ setCellHighlight(cellId ? String(cellId) : null);
160
+ }
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Emits a function to project from the
166
+ * cell ID space to the scatterplot or
167
+ * spatial coordinate space, via the
168
+ * `updateViewInfo` prop.
169
+ */
170
+ viewInfoDidUpdate(obsIndex, obsLocations, makeGetObsCoords) {
171
+ const { updateViewInfo, uuid } = this.props;
172
+ const { viewport } = this;
173
+ if (updateViewInfo && viewport) {
174
+ updateViewInfo({
175
+ uuid,
176
+ project: (obsId) => {
177
+ try {
178
+ if (obsIndex && obsLocations) {
179
+ const getObsCoords = makeGetObsCoords(obsLocations);
180
+ const obsIdx = obsIndex.indexOf(obsId);
181
+ const obsCoord = getObsCoords(obsIdx);
182
+ return viewport.project(obsCoord);
183
+ }
184
+ return [null, null];
185
+ } catch (e) {
186
+ return [null, null];
187
+ }
188
+ },
189
+ });
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Intended to be overridden by descendants.
195
+ */
196
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
197
+ componentDidUpdate() {
198
+
199
+ }
200
+
201
+ /**
202
+ * Intended to be overridden by descendants.
203
+ * @returns {boolean} Whether or not any layers are 3D.
204
+ */
205
+ // eslint-disable-next-line class-methods-use-this
206
+ use3d() {
207
+ return false;
208
+ }
209
+
210
+ /**
211
+ * A common render function for both Spatial
212
+ * and Scatterplot components.
213
+ */
214
+ render() {
215
+ const {
216
+ deckRef, viewState, uuid, hideTools,
217
+ } = this.props;
218
+ const { gl, tool } = this.state;
219
+ const layers = this.getLayers();
220
+ const use3d = this.use3d();
221
+
222
+ const showCellSelectionTools = this.obsSegmentationsData !== null;
223
+ const showPanTool = layers.length > 0;
224
+ // For large datasets or ray casting, the visual quality takes only a small
225
+ // hit in exchange for much better performance by setting this to false:
226
+ // https://deck.gl/docs/api-reference/core/deck#usedevicepixels
227
+ const useDevicePixels = (!use3d
228
+ && (
229
+ this.obsSegmentationsData?.shape?.[0] < 100000
230
+ || this.obsLocationsData?.shape?.[1] < 100000
231
+ )
232
+ );
233
+
234
+ return (
235
+ <>
236
+ <ToolMenu
237
+ activeTool={tool}
238
+ setActiveTool={this.onToolChange}
239
+ visibleTools={{
240
+ pan: showPanTool && !hideTools,
241
+ selectRectangle: showCellSelectionTools && !hideTools,
242
+ selectLasso: showCellSelectionTools && !hideTools,
243
+ }}
244
+ />
245
+ <deck.DeckGL
246
+ id={`deckgl-overlay-${uuid}`}
247
+ ref={deckRef}
248
+ views={[
249
+ use3d
250
+ ? new deck.OrbitView({ id: 'orbit', controller: true, orbitAxis: 'Y' })
251
+ : new deck.OrthographicView({
252
+ id: 'ortho',
253
+ }),
254
+ ]} // id is a fix for https://github.com/uber/deck.gl/issues/3259
255
+ layers={
256
+ gl && viewState.target.slice(0, 2).every(i => typeof i === 'number')
257
+ ? layers
258
+ : []
259
+ }
260
+ glOptions={DEFAULT_GL_OPTIONS}
261
+ onWebGLInitialized={this.onWebGLInitialized}
262
+ onViewStateChange={this.onViewStateChange}
263
+ viewState={viewState}
264
+ useDevicePixels={useDevicePixels}
265
+ controller={tool ? { dragPan: false } : true}
266
+ getCursor={tool ? getCursorWithTool : getCursor}
267
+ onHover={this.onHover}
268
+ >
269
+ {this.onInitializeViewInfo}
270
+ </deck.DeckGL>
271
+ </>
272
+ );
273
+ }
274
+ }
@@ -0,0 +1,105 @@
1
+ import React from 'react';
2
+ import clsx from 'clsx';
3
+ import { SELECTION_TYPE } from '@vitessce/gl';
4
+ import { PointerIconSVG, SelectRectangleIconSVG, SelectLassoIconSVG } from '@vitessce/icons';
5
+ import { makeStyles } from '@material-ui/core';
6
+
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
+
25
+ display: 'inline-block',
26
+ fontWeight: '400',
27
+ textAlign: 'center',
28
+ verticalAlign: 'middle',
29
+ cursor: 'pointer',
30
+ userSelect: 'none',
31
+ border: '1px solid #6c757d',
32
+ fontSize: '16px',
33
+ lineHeight: '1.5',
34
+ borderRadius: '4px',
35
+ 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',
36
+ color: '#6c757d',
37
+
38
+ marginRight: '8px',
39
+
40
+ '& > svg': {
41
+ verticalAlign: 'middle',
42
+ },
43
+ },
44
+ iconButtonActive: {
45
+ // active
46
+ color: '#fff',
47
+ backgroundColor: '#6c757d',
48
+ borderColor: '#6c757d',
49
+ boxShadow: '0 0 0 3px rgba(108, 117, 125, 0.5)',
50
+ },
51
+ }));
52
+
53
+ export function IconButton(props) {
54
+ const {
55
+ alt, onClick, isActive, children,
56
+ } = props;
57
+ const classes = useStyles();
58
+ return (
59
+ <button
60
+ className={clsx(classes.iconButton, { [classes.iconButtonActive]: isActive })}
61
+ onClick={onClick}
62
+ type="button"
63
+ title={alt}
64
+ >
65
+ {children}
66
+ </button>
67
+ );
68
+ }
69
+
70
+ export default function ToolMenu(props) {
71
+ const {
72
+ setActiveTool,
73
+ activeTool,
74
+ visibleTools = { pan: true, selectRectangle: true, selectLasso: true },
75
+ } = props;
76
+ const classes = useStyles();
77
+ return (
78
+ <div className={classes.tool}>
79
+ {visibleTools.pan && (
80
+ <IconButton
81
+ alt="pointer tool"
82
+ onClick={() => setActiveTool(null)}
83
+ isActive={activeTool === null}
84
+ ><PointerIconSVG />
85
+ </IconButton>
86
+ )}
87
+ {visibleTools.selectRectangle ? (
88
+ <IconButton
89
+ alt="select rectangle"
90
+ onClick={() => setActiveTool(SELECTION_TYPE.RECTANGLE)}
91
+ isActive={activeTool === SELECTION_TYPE.RECTANGLE}
92
+ ><SelectRectangleIconSVG />
93
+ </IconButton>
94
+ ) : null}
95
+ {visibleTools.selectLasso ? (
96
+ <IconButton
97
+ alt="select lasso"
98
+ onClick={() => setActiveTool(SELECTION_TYPE.POLYGON)}
99
+ isActive={activeTool === SELECTION_TYPE.POLYGON}
100
+ ><SelectLassoIconSVG />
101
+ </IconButton>
102
+ ) : null}
103
+ </div>
104
+ );
105
+ }
@@ -0,0 +1,18 @@
1
+ import '@testing-library/jest-dom';
2
+ import { cleanup, render, screen } from '@testing-library/react'
3
+ import { afterEach, expect } from 'vitest'
4
+
5
+ import { IconButton } from './ToolMenu';
6
+
7
+ afterEach(() => {
8
+ cleanup()
9
+ });
10
+
11
+ describe('ToolMenu.js', () => {
12
+ describe('<IconButton />', () => {
13
+ it('renders with title attribute', () => {
14
+ const { container } = render(<IconButton isActive alt="Lasso" />);
15
+ expect(container.querySelectorAll('[title="Lasso"]').length).toEqual(1);
16
+ });
17
+ });
18
+ });