@haniffalab/cherita-react 2.0.0 → 2.1.0

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 (67) hide show
  1. package/dist/cjs/components/dotplot/Dotplot.js +4 -5
  2. package/dist/cjs/components/dotplot/DotplotControls.js +7 -3
  3. package/dist/cjs/components/heatmap/Heatmap.js +4 -5
  4. package/dist/cjs/components/icons/HeatmapIcon.js +2 -2
  5. package/dist/cjs/components/matrixplot/Matrixplot.js +4 -5
  6. package/dist/cjs/components/obs-list/ObsItem.js +7 -7
  7. package/dist/cjs/components/offcanvas/OffCanvas.js +7 -4
  8. package/dist/cjs/components/plot/PlotTypeSelector.js +49 -10
  9. package/dist/cjs/components/pseudospatial/Pseudospatial.js +8 -5
  10. package/dist/cjs/components/scatterplot/Scatterplot.js +134 -136
  11. package/dist/cjs/components/scatterplot/ScatterplotLayer.js +144 -0
  12. package/dist/cjs/components/scatterplot/SpatialControls.js +7 -4
  13. package/dist/cjs/components/scatterplot/Toolbox.js +8 -4
  14. package/dist/cjs/components/search-bar/SearchBar.js +51 -29
  15. package/dist/cjs/components/search-bar/SearchInfo.js +2 -2
  16. package/dist/cjs/components/search-bar/SearchResults.js +9 -6
  17. package/dist/cjs/components/toolbar/Toolbar.js +10 -65
  18. package/dist/cjs/components/var-list/VarItem.js +4 -6
  19. package/dist/cjs/components/var-list/VarList.js +17 -9
  20. package/dist/cjs/components/var-list/VarListToolbar.js +1 -1
  21. package/dist/cjs/components/var-list/VarSet.js +7 -5
  22. package/dist/cjs/components/violin/Violin.js +6 -7
  23. package/dist/cjs/constants/constants.js +6 -3
  24. package/dist/cjs/context/DatasetContext.js +11 -2
  25. package/dist/cjs/context/SettingsContext.js +27 -2
  26. package/dist/cjs/helpers/color-helper.js +17 -12
  27. package/dist/cjs/index.js +0 -7
  28. package/dist/cjs/utils/Legend.js +6 -5
  29. package/dist/cjs/utils/requests.js +2 -2
  30. package/dist/cjs/views/ObservationFeature/StandardView.js +1 -1
  31. package/dist/cjs/views/PerturbationMap/ObsExplorer.js +11 -9
  32. package/dist/cjs/views/PerturbationMap/StandardView.js +2 -1
  33. package/dist/cjs/workers/scatterplotData.js +16 -0
  34. package/dist/esm/components/dotplot/Dotplot.js +5 -6
  35. package/dist/esm/components/dotplot/DotplotControls.js +6 -3
  36. package/dist/esm/components/heatmap/Heatmap.js +5 -6
  37. package/dist/esm/components/icons/HeatmapIcon.js +2 -2
  38. package/dist/esm/components/matrixplot/Matrixplot.js +5 -6
  39. package/dist/esm/components/obs-list/ObsItem.js +7 -7
  40. package/dist/esm/components/offcanvas/OffCanvas.js +7 -4
  41. package/dist/esm/components/plot/PlotTypeSelector.js +49 -10
  42. package/dist/esm/components/pseudospatial/Pseudospatial.js +8 -5
  43. package/dist/esm/components/scatterplot/Scatterplot.js +132 -134
  44. package/dist/esm/components/scatterplot/ScatterplotLayer.js +137 -0
  45. package/dist/esm/components/scatterplot/SpatialControls.js +7 -4
  46. package/dist/esm/components/scatterplot/Toolbox.js +8 -4
  47. package/dist/esm/components/search-bar/SearchBar.js +52 -30
  48. package/dist/esm/components/search-bar/SearchInfo.js +2 -2
  49. package/dist/esm/components/search-bar/SearchResults.js +9 -6
  50. package/dist/esm/components/toolbar/Toolbar.js +9 -63
  51. package/dist/esm/components/var-list/VarItem.js +4 -6
  52. package/dist/esm/components/var-list/VarList.js +17 -9
  53. package/dist/esm/components/var-list/VarListToolbar.js +1 -1
  54. package/dist/esm/components/var-list/VarSet.js +7 -5
  55. package/dist/esm/components/violin/Violin.js +7 -8
  56. package/dist/esm/constants/constants.js +5 -2
  57. package/dist/esm/context/DatasetContext.js +11 -2
  58. package/dist/esm/context/SettingsContext.js +27 -2
  59. package/dist/esm/helpers/color-helper.js +17 -12
  60. package/dist/esm/index.js +0 -1
  61. package/dist/esm/utils/Legend.js +6 -5
  62. package/dist/esm/utils/requests.js +2 -2
  63. package/dist/esm/views/ObservationFeature/StandardView.js +1 -1
  64. package/dist/esm/views/PerturbationMap/ObsExplorer.js +11 -9
  65. package/dist/esm/views/PerturbationMap/StandardView.js +2 -1
  66. package/dist/esm/workers/scatterplotData.js +10 -0
  67. package/package.json +6 -3
@@ -85,7 +85,8 @@ export function Pseudospatial(_ref) {
85
85
  setPlotType
86
86
  } = _ref;
87
87
  const {
88
- imageUrl
88
+ imageUrl,
89
+ valueLabel
89
90
  } = useDataset();
90
91
  const settings = useSettings();
91
92
  const dispatch = useSettingsDispatch();
@@ -93,7 +94,9 @@ export function Pseudospatial(_ref) {
93
94
  const [layout, setLayout] = useState({});
94
95
  const {
95
96
  getColor
96
- } = useColor();
97
+ } = useColor({
98
+ isCategorical: false
99
+ });
97
100
  const colorscale = useRef(settings.controls.colorScale);
98
101
  const {
99
102
  valueMin,
@@ -129,7 +132,7 @@ export function Pseudospatial(_ref) {
129
132
  return trace;
130
133
  }
131
134
  const color = rgbToHex(getColor({
132
- value: (v - min) / (max - min)
135
+ value: (v - min) / Math.max(max - min, 1e-6)
133
136
  }));
134
137
  return _objectSpread(_objectSpread({}, trace), {}, {
135
138
  fillcolor: color,
@@ -172,7 +175,7 @@ export function Pseudospatial(_ref) {
172
175
  return trace;
173
176
  }
174
177
  const color = rgbToHex(getColor({
175
- value: (v - min) / (max - min)
178
+ value: (v - min) / Math.max(max - min, 1e-6)
176
179
  }));
177
180
  return _objectSpread(_objectSpread({}, trace), {}, {
178
181
  fillcolor: color,
@@ -271,7 +274,7 @@ export function Pseudospatial(_ref) {
271
274
  }), hasSelections && showLegend && /*#__PURE__*/_jsx(Legend, {
272
275
  min: layout === null || layout === void 0 || (_layout$coloraxis5 = layout.coloraxis) === null || _layout$coloraxis5 === void 0 ? void 0 : _layout$coloraxis5.cmin,
273
276
  max: layout === null || layout === void 0 || (_layout$coloraxis6 = layout.coloraxis) === null || _layout$coloraxis6 === void 0 ? void 0 : _layout$coloraxis6.cmax,
274
- addText: plotType === PLOT_TYPES.GENE ? ' - Mean expression' : plotType === PLOT_TYPES.CATEGORICAL ? ' - %' : plotType === PLOT_TYPES.CONTINUOUS ? ' - Mean value' : ''
277
+ addText: plotType === PLOT_TYPES.GENE ? " - Mean ".concat(valueLabel) : plotType === PLOT_TYPES.CATEGORICAL ? ' - %' : plotType === PLOT_TYPES.CONTINUOUS ? ' - Mean value' : ''
275
278
  })]
276
279
  })
277
280
  });
@@ -5,7 +5,6 @@ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol"
5
5
  function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
6
6
  import { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react';
7
7
  import { LinearInterpolator } from '@deck.gl/core';
8
- import { ScatterplotLayer } from '@deck.gl/layers';
9
8
  import { DeckGL } from '@deck.gl/react';
10
9
  import { faTriangleExclamation } from '@fortawesome/free-solid-svg-icons';
11
10
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -13,9 +12,10 @@ import { ViewMode } from '@nebula.gl/edit-modes';
13
12
  import { EditableGeoJsonLayer } from '@nebula.gl/layers';
14
13
  import _ from 'lodash';
15
14
  import { Alert } from 'react-bootstrap';
15
+ import { ScatterplotLayer } from './ScatterplotLayer';
16
16
  import { SpatialControls } from './SpatialControls';
17
17
  import { Toolbox } from './Toolbox';
18
- import { COLOR_ENCODINGS, OBS_TYPES, SELECTED_POLYGON_FILLCOLOR, UNSELECTED_POLYGON_FILLCOLOR } from '../../constants/constants';
18
+ import { COLOR_ENCODINGS, OBS_TYPES, PLOT_TYPES, SELECTED_POLYGON_FILLCOLOR, UNSELECTED_POLYGON_FILLCOLOR, GRAY } from '../../constants/constants';
19
19
  import { useDataset } from '../../context/DatasetContext';
20
20
  import { useFilteredData } from '../../context/FilterContext';
21
21
  import { useSettings, useSettingsDispatch } from '../../context/SettingsContext';
@@ -28,6 +28,7 @@ import { useSelectedObs } from '../../utils/Resolver';
28
28
  import { formatNumerical } from '../../utils/string';
29
29
  import usePlotVisibility from '../../utils/usePlotVisibility';
30
30
  import { useLabelObsData } from '../../utils/zarrData';
31
+ import { createScatterplotWorker } from '../../workers/scatterplotData.js';
31
32
  import { PlotAlert } from '../plot/PlotAlert';
32
33
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
33
34
  window.deck.log.level = 1;
@@ -48,15 +49,15 @@ const getRadiusScale = bounds => {
48
49
  return rs;
49
50
  };
50
51
  export function Scatterplot(_ref) {
51
- var _features$features2, _obsmData$serverError, _labelObsData$serverE, _settings$selectedVar, _data$positions2;
52
+ var _data$positions5, _features$features2, _obsmData$serverError, _labelObsData$serverE, _settings$selectedVar, _data$positions6;
52
53
  let {
54
+ pointInteractionEnabled = false,
55
+ showSpatialControls = true,
53
56
  setShowCategories,
54
57
  setShowSearch,
55
- plotType,
56
58
  setPlotType,
57
59
  isFullscreen = false,
58
- pointInteractionEnabled = false,
59
- showSpatialControls = true
60
+ isSearchObs
60
61
  } = _ref;
61
62
  const {
62
63
  useUnsColors
@@ -69,9 +70,6 @@ export function Scatterplot(_ref) {
69
70
  slicedLength
70
71
  } = useFilteredData();
71
72
  const dispatch = useSettingsDispatch();
72
- const {
73
- getColor
74
- } = useColor();
75
73
  const deckRef = useRef(null);
76
74
  const [viewport, setViewport] = useState(null);
77
75
  const [viewState, setViewState] = useState(INITIAL_VIEW_STATE);
@@ -87,8 +85,6 @@ export function Scatterplot(_ref) {
87
85
  const radiusScale = settings.controls.radiusScale[settings.selectedObsm] || 1;
88
86
  const selectedObs = useSelectedObs();
89
87
  const selectedObsIndex = settings.selectedObsIndex;
90
- const [hoveredIndex, setHoveredIndex] = useState(null);
91
- const [isHoveringPoint, setIsHoveringPoint] = useState(false);
92
88
  const {
93
89
  showSearchBtn
94
90
  } = usePlotVisibility(isFullscreen);
@@ -107,6 +103,22 @@ export function Scatterplot(_ref) {
107
103
  } = useZarrData();
108
104
  const labelObsData = useLabelObsData();
109
105
  const clickedInsideRef = useRef(false);
106
+ const workerRef = useRef(null);
107
+ const [scatterplotAttributes, setScatterplotAttributes] = useState(null);
108
+ const isCategorical = useMemo(() => {
109
+ if (settings.colorEncoding === COLOR_ENCODINGS.OBS) {
110
+ return (selectedObs === null || selectedObs === void 0 ? void 0 : selectedObs.type) === OBS_TYPES.CATEGORICAL || (selectedObs === null || selectedObs === void 0 ? void 0 : selectedObs.type) === OBS_TYPES.BOOLEAN;
111
+ } else {
112
+ return false;
113
+ }
114
+ }, [settings.colorEncoding, selectedObs === null || selectedObs === void 0 ? void 0 : selectedObs.type]);
115
+ const {
116
+ colormap,
117
+ getColor
118
+ } = useColor({
119
+ isCategorical,
120
+ colorscale: useUnsColors && settings.colorEncoding === COLOR_ENCODINGS.OBS ? selectedObs === null || selectedObs === void 0 ? void 0 : selectedObs.colors : null
121
+ });
110
122
 
111
123
  // @TODO: assert length of obsmData, xData, obsData is equal
112
124
 
@@ -156,7 +168,8 @@ export function Scatterplot(_ref) {
156
168
  }
157
169
  }, [obsData.data, obsData.isPending, obsData.serverError, obsmData.data, obsmData.isPending, obsmData.serverError, settings.colorEncoding, xData.data, xData.isPending, xData.serverError]);
158
170
  useEffect(() => {
159
- if (data.positions && data.positions.length) {
171
+ var _data$positions;
172
+ if ((_data$positions = data.positions) !== null && _data$positions !== void 0 && _data$positions.length) {
160
173
  var _deckRef$current, _deckRef$current2;
161
174
  const mapHelper = new MapHelper();
162
175
  const {
@@ -191,10 +204,10 @@ export function Scatterplot(_ref) {
191
204
  }
192
205
  }, [dispatch, settings.controls.radiusScale, settings.selectedObsm, viewport === null || viewport === void 0 ? void 0 : viewport.bounds]);
193
206
  useEffect(() => {
194
- var _data$positions;
207
+ var _data$positions2;
195
208
  if (!pointInteractionEnabled) return;
196
209
  if (selectedObsIndex == null) return;
197
- if (!((_data$positions = data.positions) !== null && _data$positions !== void 0 && _data$positions.length)) return;
210
+ if (!((_data$positions2 = data.positions) !== null && _data$positions2 !== void 0 && _data$positions2.length)) return;
198
211
 
199
212
  // If the selection came from a click inside this plot, skip recentering
200
213
  if (clickedInsideRef.current) {
@@ -229,116 +242,110 @@ export function Scatterplot(_ref) {
229
242
  zoom
230
243
  };
231
244
  }, [data.positions]);
232
-
233
- // Make stable references for getOriginalIndex and sortedIndexMap
234
- const identityGetOriginalIndex = useCallback(i => i, []);
235
- const identitySortedIndexMap = useMemo(() => ({
236
- get: key => key
237
- }), []);
238
- const {
239
- sortedData,
240
- getOriginalIndex,
241
- sortedIndexMap
242
- } = useMemo(() => {
243
- if ((settings.colorEncoding === COLOR_ENCODINGS.VAR || settings.colorEncoding === COLOR_ENCODINGS.OBS && (selectedObs === null || selectedObs === void 0 ? void 0 : selectedObs.type) === OBS_TYPES.CONTINUOUS) && data.positions && data.values && data.positions.length === data.values.length) {
244
- const sortedIndices = _.map(data.values, (_v, i) => i).sort((a, b) => data.values[a] - data.values[b]);
245
- const sortedIndexMap = new Map(_.map(sortedIndices, (originalIndex, sortedIndex) => [originalIndex, sortedIndex]));
246
- return {
247
- sortedData: _.mapValues(data, (v, _k) => {
248
- return v ? _.at(v, sortedIndices) : v;
249
- }),
250
- getOriginalIndex: i => sortedIndices[i],
251
- sortedIndexMap: sortedIndexMap
252
- };
253
- }
254
- return {
255
- sortedData: data,
256
- getOriginalIndex: identityGetOriginalIndex,
257
- // return original index
258
- sortedIndexMap: identitySortedIndexMap // return original index
259
- };
260
- }, [data, identityGetOriginalIndex, identitySortedIndexMap, selectedObs === null || selectedObs === void 0 ? void 0 : selectedObs.type, settings.colorEncoding]);
261
- const hoverLayer = typeof hoveredIndex === 'number' && Array.isArray(sortedData === null || sortedData === void 0 ? void 0 : sortedData.positions) && hoveredIndex < sortedData.positions.length ? new ScatterplotLayer({
262
- id: 'hover-highlight',
263
- data: [sortedData.positions[hoveredIndex]],
264
- getPosition: d => d,
265
- getFillColor: [255, 215, 0, 180],
266
- getRadius: 10,
267
- radiusMinPixels: 15,
268
- radiusScale: 1,
269
- pointSizeUnits: 'pixels',
270
- pickable: false,
271
- parameters: {
272
- depthTest: false
273
- }
274
- }) : null;
275
- const sortedObsIndices = useMemo(() => {
276
- return obsIndices ? new Set(Array.from(obsIndices, i => sortedIndexMap.get(i))) : obsIndices;
277
- }, [obsIndices, sortedIndexMap]);
278
- const isCategorical = useMemo(() => {
279
- if (settings.colorEncoding === COLOR_ENCODINGS.OBS) {
280
- return (selectedObs === null || selectedObs === void 0 ? void 0 : selectedObs.type) === OBS_TYPES.CATEGORICAL || (selectedObs === null || selectedObs === void 0 ? void 0 : selectedObs.type) === OBS_TYPES.BOOLEAN;
281
- } else {
282
- return false;
283
- }
284
- }, [settings.colorEncoding, selectedObs === null || selectedObs === void 0 ? void 0 : selectedObs.type]);
285
245
  const {
286
246
  min,
287
247
  max
288
- } = {
248
+ } = useMemo(() => ({
289
249
  min: settings.controls.range[0] * (valueMax - valueMin) + valueMin,
290
250
  max: settings.controls.range[1] * (valueMax - valueMin) + valueMin
291
- };
292
- const getFillColor = useCallback((_d, _ref2) => {
293
- let {
294
- index
295
- } = _ref2;
296
- const grayOut = isPending || sortedObsIndices && !sortedObsIndices.has(index);
297
- if (pointInteractionEnabled && getOriginalIndex(index) === selectedObsIndex) {
298
- return [255, 215, 0, 255];
251
+ }), [settings.controls.range, valueMax, valueMin]);
252
+ useEffect(() => {
253
+ workerRef.current = createScatterplotWorker();
254
+ workerRef.current.onmessage = _ref2 => {
255
+ let {
256
+ data
257
+ } = _ref2;
258
+ setScatterplotAttributes(p => _objectSpread(_objectSpread({}, p), data));
259
+ };
260
+ workerRef.current.onerror = e => {
261
+ console.error('Worker error:', e);
262
+ };
263
+ return () => {
264
+ var _workerRef$current;
265
+ (_workerRef$current = workerRef.current) === null || _workerRef$current === void 0 || _workerRef$current.terminate();
266
+ };
267
+ }, []);
268
+ useEffect(() => {
269
+ var _data$positions3;
270
+ if (workerRef.current && (_data$positions3 = data.positions) !== null && _data$positions3 !== void 0 && _data$positions3.length) {
271
+ workerRef.current.postMessage({
272
+ positions: data.positions
273
+ });
299
274
  }
300
- return getColor(_objectSpread({
301
- value: (sortedData.values[index] - min) / (max - min),
302
- categorical: isCategorical,
303
- grayOut: grayOut
304
- }, useUnsColors && settings.colorEncoding === COLOR_ENCODINGS.OBS && selectedObs !== null && selectedObs !== void 0 && selectedObs.colors ? {
305
- colorscale: selectedObs === null || selectedObs === void 0 ? void 0 : selectedObs.colors
306
- } : {})) || [0, 0, 0, 100];
307
- }, [isPending, sortedObsIndices, pointInteractionEnabled, getOriginalIndex, selectedObsIndex, getColor, sortedData.values, min, max, isCategorical, useUnsColors, settings.colorEncoding, selectedObs === null || selectedObs === void 0 ? void 0 : selectedObs.colors]);
308
-
309
- // @TODO: add support for pseudospatial hover to reflect in radius
310
- const getRadius = useCallback((_d, _ref3) => {
275
+ }, [data.positions]);
276
+ useEffect(() => {
277
+ var _data$values;
278
+ if (workerRef.current && (_data$values = data.values) !== null && _data$values !== void 0 && _data$values.length) {
279
+ workerRef.current.postMessage({
280
+ values: data.values
281
+ });
282
+ }
283
+ }, [data.values]);
284
+ useEffect(() => {
285
+ var _data$positions4;
286
+ if (workerRef.current && (_data$positions4 = data.positions) !== null && _data$positions4 !== void 0 && _data$positions4.length && obsIndices !== undefined) {
287
+ workerRef.current.postMessage({
288
+ obsIndices,
289
+ length: data.positions.length
290
+ });
291
+ }
292
+ }, [obsIndices, (_data$positions5 = data.positions) === null || _data$positions5 === void 0 ? void 0 : _data$positions5.length]);
293
+ const getFillColor = useCallback((_d, _ref3) => {
311
294
  let {
312
295
  index
313
296
  } = _ref3;
314
- const grayOut = sortedObsIndices && !sortedObsIndices.has(index);
315
- if (pointInteractionEnabled && getOriginalIndex(index) === selectedObsIndex) {
316
- return 50;
297
+ const grayOut = isPending || obsIndices && !obsIndices.has(index);
298
+ if (pointInteractionEnabled && index === selectedObsIndex) {
299
+ return [255, 215, 0, 255];
317
300
  }
318
- return (grayOut ? 1 : 3) * (pointInteractionEnabled ? 26 : 1);
319
- }, [getOriginalIndex, pointInteractionEnabled, selectedObsIndex, sortedObsIndices]);
301
+ return getColor({
302
+ value: (data.values[index] - min) / Math.max(max - min, 1e-6),
303
+ grayOut: grayOut
304
+ }) || [0, 0, 0, 100];
305
+ }, [isPending, obsIndices, pointInteractionEnabled, selectedObsIndex, getColor, data.values, min, max]);
320
306
  const memoizedLayers = useMemo(() => {
307
+ const hasSelection = settings.colorEncoding === COLOR_ENCODINGS.VAR && settings.selectedVar || settings.colorEncoding === COLOR_ENCODINGS.OBS && selectedObs;
321
308
  return [new ScatterplotLayer({
322
309
  id: 'cherita-layer-scatterplot',
323
310
  pickable: true,
324
- data: sortedData.positions,
311
+ autoHighlight: true,
312
+ highlightColor: pointInteractionEnabled ? [255, 215, 0, 255] : [0, 0, 0, 0],
313
+ data: {
314
+ length: (scatterplotAttributes === null || scatterplotAttributes === void 0 ? void 0 : scatterplotAttributes.count) || 0,
315
+ attributes: {
316
+ getPosition: {
317
+ value: (scatterplotAttributes === null || scatterplotAttributes === void 0 ? void 0 : scatterplotAttributes.positions) || new Float32Array(0),
318
+ size: 2
319
+ },
320
+ getValues: {
321
+ value: scatterplotAttributes === null || scatterplotAttributes === void 0 ? void 0 : scatterplotAttributes.values,
322
+ size: 1
323
+ },
324
+ getEnabled: {
325
+ value: scatterplotAttributes === null || scatterplotAttributes === void 0 ? void 0 : scatterplotAttributes.indexEnabledBitmask,
326
+ size: 1
327
+ }
328
+ }
329
+ },
325
330
  radiusScale: radiusScale,
326
331
  radiusMinPixels: 1,
327
- getPosition: d => d,
328
- getFillColor: getFillColor,
329
- getRadius: getRadius,
330
332
  updateTriggers: {
331
- getFillColor: getFillColor,
332
- getRadius: [getRadius, hoveredIndex, selectedObsIndex]
333
+ colormap: [colormap]
333
334
  },
334
- transitions: {
335
- getRadius: 200,
336
- getFillColor: 200
337
- }
335
+ colormap: isPending ? ["".concat(rgbToHex(GRAY), "aa")] : hasSelection ? colormap : ['#000000aa'],
336
+ isCategorical,
337
+ valueMin: min,
338
+ valueMax: max,
339
+ selectedIndex: pointInteractionEnabled ? selectedObsIndex !== null && selectedObsIndex !== void 0 ? selectedObsIndex : -1 : -1,
340
+ pointInteractionEnabled,
341
+ highlightMultiplier: pointInteractionEnabled ? 1.5 : 1.0
338
342
  }), new EditableGeoJsonLayer({
339
343
  id: 'cherita-layer-draw',
340
344
  data: features,
341
345
  mode: mode,
346
+ parameters: {
347
+ depthTest: false
348
+ },
342
349
  selectedFeatureIndexes,
343
350
  onEdit: _ref4 => {
344
351
  let {
@@ -369,12 +376,9 @@ export function Scatterplot(_ref) {
369
376
  }
370
377
  }
371
378
  })];
372
- }, [sortedData.positions, features, getFillColor, getRadius, hoveredIndex, mode, radiusScale, selectedFeatureIndexes, selectedObsIndex]);
379
+ }, [settings.colorEncoding, settings.selectedVar, selectedObs, pointInteractionEnabled, scatterplotAttributes === null || scatterplotAttributes === void 0 ? void 0 : scatterplotAttributes.count, scatterplotAttributes === null || scatterplotAttributes === void 0 ? void 0 : scatterplotAttributes.positions, scatterplotAttributes === null || scatterplotAttributes === void 0 ? void 0 : scatterplotAttributes.values, scatterplotAttributes === null || scatterplotAttributes === void 0 ? void 0 : scatterplotAttributes.indexEnabledBitmask, radiusScale, colormap, isPending, isCategorical, min, max, selectedObsIndex, features, mode, selectedFeatureIndexes]);
380
+ const layers = useDeferredValue(mode === ViewMode ? [...memoizedLayers].reverse() : memoizedLayers); // draw scatterplot on top of polygons when in ViewMode
373
381
 
374
- // const layers = useDeferredValue(
375
- // mode === ViewMode ? memoizedLayers.reverse() : memoizedLayers,
376
- // ); // draw scatterplot on top of polygons when in ViewMode
377
- const layers = useDeferredValue([...memoizedLayers, hoverLayer].filter(Boolean));
378
382
  useEffect(() => {
379
383
  var _features$features;
380
384
  if (!(features !== null && features !== void 0 && (_features$features = features.features) !== null && _features$features !== void 0 && _features$features.length)) {
@@ -391,22 +395,22 @@ export function Scatterplot(_ref) {
391
395
  });
392
396
  }, [settings.selectedObsm, dispatch, features.features]);
393
397
  function onLayerClick(info) {
398
+ var _info$layer, _info$layer2;
394
399
  if (mode !== ViewMode) return;
395
- if (!info.object) {
400
+ if (info.index === undefined || info.index === null) {
396
401
  // clicked empty space
397
402
  setSelectedFeatureIndexes([]);
398
403
  return;
399
404
  }
400
- if (info.layer.id === 'cherita-layer-draw') {
405
+ if (((_info$layer = info.layer) === null || _info$layer === void 0 ? void 0 : _info$layer.id) === 'cherita-layer-draw') {
401
406
  // clicked a drawn polygon
402
407
  setSelectedFeatureIndexes([info.index]);
403
- } else if (info.layer.id === 'cherita-layer-scatterplot' && pointInteractionEnabled) {
408
+ } else if (((_info$layer2 = info.layer) === null || _info$layer2 === void 0 ? void 0 : _info$layer2.id) === 'cherita-layer-scatterplot' && pointInteractionEnabled) {
404
409
  // clicked a scatterplot point
405
410
  clickedInsideRef.current = true;
406
- const originalIndex = getOriginalIndex(info.index);
407
411
  dispatch({
408
412
  type: 'set.selectedObsIndex',
409
- index: originalIndex
413
+ index: info.index
410
414
  });
411
415
  // in collapsed view, open offcanvas
412
416
  if (pointInteractionEnabled && showSearchBtn) {
@@ -431,25 +435,26 @@ export function Scatterplot(_ref) {
431
435
  object,
432
436
  index
433
437
  } = _ref5;
434
- if (!object || (object === null || object === void 0 ? void 0 : object.type) === 'Feature') return;
438
+ if ((object === null || object === void 0 ? void 0 : object.type) === 'Feature') return;
439
+ if (index < 0 || index === null) return;
435
440
  const text = [];
436
441
  if (settings.colorEncoding === COLOR_ENCODINGS.OBS && selectedObs && !_.includes(settings.labelObs, selectedObs.name)) {
437
- var _data$values;
438
- text.push(getLabel(selectedObs, (_data$values = data.values) === null || _data$values === void 0 ? void 0 : _data$values[getOriginalIndex(index)]));
442
+ var _data$values2;
443
+ text.push(getLabel(selectedObs, (_data$values2 = data.values) === null || _data$values2 === void 0 ? void 0 : _data$values2[index]));
439
444
  }
440
445
  if (settings.colorEncoding === COLOR_ENCODINGS.VAR && settings.selectedVar) {
441
- var _data$values2;
442
- text.push(getLabel(settings.selectedVar, (_data$values2 = data.values) === null || _data$values2 === void 0 ? void 0 : _data$values2[getOriginalIndex(index)], true));
446
+ var _data$values3;
447
+ text.push(getLabel(settings.selectedVar, (_data$values3 = data.values) === null || _data$values3 === void 0 ? void 0 : _data$values3[index], true));
443
448
  }
444
449
  if (settings.labelObs.length) {
445
450
  text.push(..._.map(labelObsData.data, (v, k) => {
446
451
  if (!v) return;
447
452
  const labelObs = settings.data.obs[k];
448
- return getLabel(labelObs, v[getOriginalIndex(index)]);
453
+ return getLabel(labelObs, v[index]);
449
454
  }));
450
455
  }
451
456
  if (!text.length) return;
452
- const grayOut = sortedObsIndices && !sortedObsIndices.has(index);
457
+ const grayOut = obsIndices && !obsIndices.has(index);
453
458
  return {
454
459
  text: text.length ? _.compact(text).join('\n') : null,
455
460
  className: grayOut ? 'tooltip-grayout' : 'deck-tooltip',
@@ -467,7 +472,7 @@ export function Scatterplot(_ref) {
467
472
  return /*#__PURE__*/_jsx(PlotAlert, {
468
473
  variant: "info",
469
474
  heading: "Scatterplot unavailable for this dataset",
470
- plotType: plotType,
475
+ plotType: PLOT_TYPES.SCATTERPLOT,
471
476
  setPlotType: setPlotType,
472
477
  children: "This dataset does not include any embeddings, so a scatterplot cannot be displayed. Please choose a different plot type to explore the data."
473
478
  });
@@ -491,22 +496,14 @@ export function Scatterplot(_ref) {
491
496
  setIsRendering(false);
492
497
  },
493
498
  useDevicePixels: false,
494
- onHover: _ref6 => {
499
+ getCursor: _ref6 => {
495
500
  let {
496
- object,
497
- index
501
+ isDragging,
502
+ isHovering
498
503
  } = _ref6;
499
- const active = pointInteractionEnabled && !!object;
500
- setHoveredIndex(active ? index : null);
501
- setIsHoveringPoint(active);
502
- },
503
- getCursor: _ref7 => {
504
- let {
505
- isDragging
506
- } = _ref7;
507
504
  if (mode !== ViewMode) return 'crosshair';
508
505
  if (isDragging) return 'grabbing';
509
- if (isHoveringPoint) return 'pointer';
506
+ if (isHovering && pointInteractionEnabled) return 'pointer';
510
507
  return 'grab';
511
508
  },
512
509
  ref: deckRef
@@ -525,7 +522,8 @@ export function Scatterplot(_ref) {
525
522
  })),
526
523
  setShowCategories: setShowCategories,
527
524
  setShowSearch: setShowSearch,
528
- isFullscreen: isFullscreen
525
+ isFullscreen: isFullscreen,
526
+ isSearchObs: isSearchObs
529
527
  }), /*#__PURE__*/_jsxs("div", {
530
528
  className: "cherita-spatial-footer",
531
529
  children: [/*#__PURE__*/_jsxs("div", {
@@ -542,7 +540,7 @@ export function Scatterplot(_ref) {
542
540
  })]
543
541
  }), /*#__PURE__*/_jsx(Toolbox, {
544
542
  mode: settings.colorEncoding === COLOR_ENCODINGS.VAR ? (_settings$selectedVar = settings.selectedVar) === null || _settings$selectedVar === void 0 ? void 0 : _settings$selectedVar.name : settings.colorEncoding === COLOR_ENCODINGS.OBS ? selectedObs === null || selectedObs === void 0 ? void 0 : selectedObs.name : null,
545
- obsLength: parseInt((_data$positions2 = data.positions) === null || _data$positions2 === void 0 ? void 0 : _data$positions2.length),
543
+ obsLength: parseInt((_data$positions6 = data.positions) === null || _data$positions6 === void 0 ? void 0 : _data$positions6.length),
546
544
  slicedLength: parseInt(slicedLength),
547
545
  setHasObsm: setHasObsm
548
546
  })]
@@ -0,0 +1,137 @@
1
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
2
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
3
+ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
4
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
5
+ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
6
+ import { Texture2D } from '@luma.gl/core';
7
+ import { ScatterplotLayer as BaseLayer } from 'deck.gl';
8
+ import { GRAY, GRAY_ALPHA, GRAY_MIX } from '../../constants/constants';
9
+ export class ScatterplotLayer extends BaseLayer {
10
+ getShaders() {
11
+ const shaders = super.getShaders();
12
+ return _objectSpread(_objectSpread({}, shaders), {}, {
13
+ inject: {
14
+ 'vs:#decl': "\n in float value;\n in float indexEnabled;\n in float instanceIndex;\n out float vNormValue;\n out float vSelected;\n out float vEnabled;\n uniform float highlightMultiplier;\n uniform bool isCategorical;\n uniform float valueMin;\n uniform float valueMax;\n uniform bool pointInteractionEnabled;\n uniform float selectedIndex;\n ",
15
+ // add z-axis position based on sortValue, zMin, and zMax
16
+ // to draw higher sortValue points above lower sortValue points
17
+ // and picked points at the front
18
+ 'vs:DECKGL_FILTER_GL_POSITION': "\n float normValue = clamp((value - valueMin) / max(valueMax - valueMin, 1e-6), 0.0, 1.0);\n if (isVertexPicked(geometry.pickingColor)) {\n position.z = -position.w;\n }\n else if (indexEnabled < 0.5) {\n position.z = position.w;\n }\n else if (isCategorical) {\n if (value == -1.0) {\n position.z = position.w; // draw invalid at back\n }\n else { position.z = 0.0; }\n }\n else {\n position.z = mix(position.w, -position.w, normValue);\n }\n ",
19
+ // increase point size for hovered points
20
+ 'vs:DECKGL_FILTER_SIZE': "\n if (pointInteractionEnabled) {\n size *= 4.0;\n if(instanceIndex == selectedIndex) {\n size *= 3.0;\n }\n }\n if (isVertexPicked(geometry.pickingColor)) {\n size *= highlightMultiplier;\n }\n if (indexEnabled < 0.5) {\n size *= 0.3;\n }\n ",
21
+ // Pass colorValue to fragment shader
22
+ 'vs:DECKGL_FILTER_COLOR': "\n float normValue = clamp((value - valueMin) / max(valueMax - valueMin, 1e-6), 0.0, 1.0);\n vNormValue = normValue;\n vEnabled = indexEnabled;\n vSelected = float(instanceIndex == selectedIndex);\n ",
23
+ 'fs:#decl': "\n in float vNormValue;\n in float vEnabled;\n in float vSelected;\n uniform sampler2D colorTexture;\n uniform bool useTexture;\n uniform float colormapSize;\n uniform vec4 gray;\n uniform float grayMix;\n ",
24
+ // Sample color from texture in fragment shader
25
+ 'fs:DECKGL_FILTER_COLOR': "\n if (useTexture) {\n // Remap to texel centers so sampling matches JS interpolation\n float u = (vNormValue * (colormapSize - 1.0) + 0.5) / colormapSize;\n color = texture(colorTexture, vec2(u, 0.5));\n }\n if (vEnabled < 0.5) {\n color.rgb = mix(color.rgb, gray.rgb / 255.0, grayMix);\n color.a = gray.a / 255.0;\n }\n if(vSelected > 0.5) {\n color = picking_uHighlightColor;\n }\n "
26
+ }
27
+ });
28
+ }
29
+ initializeState() {
30
+ super.initializeState();
31
+ this.getAttributeManager().addInstanced({
32
+ value: {
33
+ size: 1,
34
+ accessor: 'getValues',
35
+ defaultValue: 0.0
36
+ },
37
+ indexEnabled: {
38
+ size: 1,
39
+ accessor: 'getEnabled',
40
+ defaultValue: 1.0
41
+ },
42
+ instanceIndex: {
43
+ size: 1,
44
+ accessor: (_, _ref) => {
45
+ let {
46
+ index
47
+ } = _ref;
48
+ return index;
49
+ },
50
+ defaultValue: 0
51
+ }
52
+ });
53
+ }
54
+ updateState(params) {
55
+ super.updateState(params);
56
+ const {
57
+ props,
58
+ oldProps,
59
+ changeFlags
60
+ } = params;
61
+ if (props.colormap !== oldProps.colormap || changeFlags.extensionsChanged) {
62
+ this._updateColorTexture(props.colormap);
63
+ }
64
+ }
65
+ _updateColorTexture(colormap) {
66
+ const {
67
+ gl
68
+ } = this.context;
69
+
70
+ // colormap is an array of hex strings
71
+ const colors = colormap.flatMap(hex => {
72
+ const r = parseInt(hex.slice(1, 3), 16);
73
+ const g = parseInt(hex.slice(3, 5), 16);
74
+ const b = parseInt(hex.slice(5, 7), 16);
75
+ const a = parseInt(hex.slice(7, 9) || 'ff', 16);
76
+ return [r, g, b, a];
77
+ });
78
+ if (this.state.colorTexture) {
79
+ this.state.colorTexture.delete();
80
+ }
81
+ const texture = new Texture2D(gl, {
82
+ data: new Uint8Array(colors),
83
+ width: colormap === null || colormap === void 0 ? void 0 : colormap.length,
84
+ height: 1,
85
+ format: gl.RGBA,
86
+ type: gl.UNSIGNED_BYTE,
87
+ parameters: {
88
+ [gl.TEXTURE_MIN_FILTER]: gl.LINEAR,
89
+ [gl.TEXTURE_MAG_FILTER]: gl.LINEAR,
90
+ [gl.TEXTURE_WRAP_S]: gl.CLAMP_TO_EDGE,
91
+ [gl.TEXTURE_WRAP_T]: gl.CLAMP_TO_EDGE
92
+ }
93
+ });
94
+ this.setState({
95
+ colorTexture: texture
96
+ });
97
+ }
98
+ draw(params) {
99
+ var _this$props$colormap$, _this$props$colormap;
100
+ const {
101
+ highlightMultiplier = 1.0,
102
+ isCategorical = false,
103
+ valueMin = 0,
104
+ valueMax = 0,
105
+ pointInteractionEnabled = false,
106
+ selectedIndex = -1,
107
+ gray = [...GRAY, 255 * GRAY_ALPHA],
108
+ grayMix = GRAY_MIX
109
+ } = this.props;
110
+ const {
111
+ colorTexture
112
+ } = this.state;
113
+ const model = this.state.model;
114
+ if (!model) {
115
+ return;
116
+ }
117
+ model.setUniforms({
118
+ highlightMultiplier,
119
+ colorTexture: colorTexture || null,
120
+ useTexture: !!colorTexture,
121
+ colormapSize: (_this$props$colormap$ = (_this$props$colormap = this.props.colormap) === null || _this$props$colormap === void 0 ? void 0 : _this$props$colormap.length) !== null && _this$props$colormap$ !== void 0 ? _this$props$colormap$ : 1,
122
+ isCategorical,
123
+ valueMin: isNaN(valueMin) ? 0 : valueMin,
124
+ valueMax: isNaN(valueMax) ? 0 : valueMax,
125
+ pointInteractionEnabled,
126
+ selectedIndex,
127
+ gray,
128
+ grayMix
129
+ });
130
+ super.draw(params);
131
+ }
132
+ finalizeState(context) {
133
+ var _this$state$colorText;
134
+ super.finalizeState(context);
135
+ (_this$state$colorText = this.state.colorTexture) === null || _this$state$colorText === void 0 || _this$state$colorText.delete();
136
+ }
137
+ }
@@ -8,6 +8,7 @@ import Button from 'react-bootstrap/Button';
8
8
  import ButtonGroup from 'react-bootstrap/ButtonGroup';
9
9
  import Dropdown from 'react-bootstrap/Dropdown';
10
10
  import { ScatterplotControls } from './ScatterplotControls';
11
+ import { useDataset } from '../../context/DatasetContext';
11
12
  import { useSettings, useSettingsDispatch } from '../../context/SettingsContext';
12
13
  import usePlotVisibility from '../../utils/usePlotVisibility';
13
14
  import { OffcanvasControls } from '../offcanvas/OffCanvas';
@@ -25,8 +26,10 @@ export function SpatialControls(_ref) {
25
26
  decreaseZoom,
26
27
  setShowCategories,
27
28
  setShowSearch,
28
- isFullscreen
29
+ isFullscreen,
30
+ isSearchObs = false
29
31
  } = _ref;
32
+ const dataset = useDataset();
30
33
  const settings = useSettings();
31
34
  const dispatch = useSettingsDispatch();
32
35
  const [showControls, setShowControls] = useState(false);
@@ -106,7 +109,7 @@ export function SpatialControls(_ref) {
106
109
  placement: "right",
107
110
  overlay: /*#__PURE__*/_jsx(Tooltip, {
108
111
  id: "tooltip-obs",
109
- children: "Browse categories"
112
+ children: "Explore categories"
110
113
  }),
111
114
  children: /*#__PURE__*/_jsx(Button, {
112
115
  size: isCompact && 'sm',
@@ -117,9 +120,9 @@ export function SpatialControls(_ref) {
117
120
  })
118
121
  }), showSearchBtn && /*#__PURE__*/_jsx(OverlayTrigger, {
119
122
  placement: "right",
120
- overlay: /*#__PURE__*/_jsx(Tooltip, {
123
+ overlay: /*#__PURE__*/_jsxs(Tooltip, {
121
124
  id: "tooltip-vars",
122
- children: "Search features"
125
+ children: ["Search", ' ', isSearchObs ? dataset.obsLabel.plural : dataset.varLabel.plural]
123
126
  }),
124
127
  children: /*#__PURE__*/_jsx(Button, {
125
128
  size: isCompact && 'sm',