@haniffalab/cherita-react 1.0.0-dev.2025-03-13.73606a74 → 1.0.0-dev.2025-03-13.274a553c

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 (99) hide show
  1. package/dist/{components → cjs/components}/full-page/FullPage.js +6 -5
  2. package/dist/{components → cjs/components}/full-page/FullPagePseudospatial.js +6 -5
  3. package/dist/{components → cjs/components}/obs-list/ObsItem.js +50 -46
  4. package/dist/{components → cjs/components}/obs-list/ObsList.js +9 -7
  5. package/dist/{components → cjs/components}/obs-list/ObsToolbar.js +12 -11
  6. package/dist/{components → cjs/components}/offcanvas/index.js +24 -20
  7. package/dist/{components → cjs/components}/pseudospatial/Pseudospatial.js +9 -8
  8. package/dist/{components → cjs/components}/pseudospatial/PseudospatialToolbar.js +4 -3
  9. package/dist/{components → cjs/components}/scatterplot/Scatterplot.js +31 -22
  10. package/dist/{components → cjs/components}/scatterplot/SpatialControls.js +11 -10
  11. package/dist/{components → cjs/components}/scatterplot/Toolbox.js +6 -5
  12. package/dist/{components → cjs/components}/search-bar/SearchBar.js +6 -5
  13. package/dist/{components → cjs/components}/search-bar/SearchResults.js +13 -17
  14. package/dist/{components → cjs/components}/var-list/VarItem.js +32 -27
  15. package/dist/{components → cjs/components}/var-list/VarList.js +14 -10
  16. package/dist/{components → cjs/components}/var-list/VarListToolbar.js +4 -3
  17. package/dist/{components → cjs/components}/var-list/VarSet.js +21 -18
  18. package/dist/{components → cjs/components}/violin/Violin.js +4 -3
  19. package/dist/{context → cjs/context}/DatasetContext.js +11 -9
  20. package/dist/{context → cjs/context}/FilterContext.js +4 -3
  21. package/dist/{context → cjs/context}/ZarrDataContext.js +4 -3
  22. package/dist/{helpers → cjs/helpers}/color-helper.js +12 -11
  23. package/dist/{helpers → cjs/helpers}/map-helper.js +8 -7
  24. package/dist/{helpers → cjs/helpers}/zarr-helper.js +25 -15
  25. package/dist/{utils → cjs/utils}/Histogram.js +12 -8
  26. package/dist/{utils → cjs/utils}/ImageViewer.js +6 -5
  27. package/dist/{utils → cjs/utils}/Legend.js +8 -7
  28. package/dist/{utils → cjs/utils}/LoadingIndicators.js +5 -4
  29. package/dist/{utils → cjs/utils}/VirtualizedList.js +10 -9
  30. package/dist/{utils → cjs/utils}/requests.js +33 -21
  31. package/dist/{utils → cjs/utils}/string.js +9 -4
  32. package/dist/{utils → cjs/utils}/zarrData.js +12 -4
  33. package/dist/css/cherita.css +10 -0
  34. package/dist/css/cherita.css.map +1 -1
  35. package/dist/esm/components/dotplot/Dotplot.js +135 -0
  36. package/dist/esm/components/dotplot/DotplotControls.js +148 -0
  37. package/dist/esm/components/full-page/FullPage.js +134 -0
  38. package/dist/esm/components/full-page/FullPagePseudospatial.js +151 -0
  39. package/dist/esm/components/heatmap/Heatmap.js +105 -0
  40. package/dist/esm/components/heatmap/HeatmapControls.js +23 -0
  41. package/dist/esm/components/matrixplot/Matrixplot.js +107 -0
  42. package/dist/esm/components/matrixplot/MatrixplotControls.js +38 -0
  43. package/dist/esm/components/obs-list/ObsItem.js +477 -0
  44. package/dist/esm/components/obs-list/ObsList.js +256 -0
  45. package/dist/esm/components/obs-list/ObsToolbar.js +58 -0
  46. package/dist/esm/components/obsm-list/ObsmList.js +72 -0
  47. package/dist/esm/components/offcanvas/index.js +67 -0
  48. package/dist/esm/components/pseudospatial/Pseudospatial.js +228 -0
  49. package/dist/esm/components/pseudospatial/PseudospatialToolbar.js +123 -0
  50. package/dist/esm/components/scatterplot/Scatterplot.js +394 -0
  51. package/dist/esm/components/scatterplot/ScatterplotControls.js +71 -0
  52. package/dist/esm/components/scatterplot/SpatialControls.js +140 -0
  53. package/dist/esm/components/scatterplot/Toolbox.js +25 -0
  54. package/dist/esm/components/search-bar/SearchBar.js +74 -0
  55. package/dist/esm/components/search-bar/SearchResults.js +139 -0
  56. package/dist/esm/components/var-list/VarItem.js +250 -0
  57. package/dist/esm/components/var-list/VarList.js +267 -0
  58. package/dist/esm/components/var-list/VarListToolbar.js +84 -0
  59. package/dist/esm/components/var-list/VarSet.js +193 -0
  60. package/dist/esm/components/violin/Violin.js +141 -0
  61. package/dist/esm/components/violin/ViolinControls.js +24 -0
  62. package/dist/esm/constants/colorscales.js +22 -0
  63. package/dist/esm/constants/constants.js +84 -0
  64. package/dist/esm/context/DatasetContext.js +572 -0
  65. package/dist/esm/context/FilterContext.js +48 -0
  66. package/dist/esm/context/ZarrDataContext.js +26 -0
  67. package/dist/esm/helpers/color-helper.js +66 -0
  68. package/dist/esm/helpers/map-helper.js +53 -0
  69. package/dist/esm/helpers/zarr-helper.js +129 -0
  70. package/dist/esm/index.js +22 -0
  71. package/dist/esm/utils/Filter.js +147 -0
  72. package/dist/esm/utils/Histogram.js +44 -0
  73. package/dist/esm/utils/ImageViewer.js +27 -0
  74. package/dist/esm/utils/Legend.js +58 -0
  75. package/dist/esm/utils/LoadingIndicators.js +22 -0
  76. package/dist/esm/utils/VirtualizedList.js +55 -0
  77. package/dist/esm/utils/errors.js +47 -0
  78. package/dist/esm/utils/requests.js +116 -0
  79. package/dist/esm/utils/search.js +39 -0
  80. package/dist/esm/utils/string.js +59 -0
  81. package/dist/esm/utils/zarrData.js +102 -0
  82. package/package.json +16 -5
  83. package/scss/cherita.scss +10 -0
  84. package/scss/components/layouts.scss +1 -0
  85. /package/dist/{components → cjs/components}/dotplot/Dotplot.js +0 -0
  86. /package/dist/{components → cjs/components}/dotplot/DotplotControls.js +0 -0
  87. /package/dist/{components → cjs/components}/heatmap/Heatmap.js +0 -0
  88. /package/dist/{components → cjs/components}/heatmap/HeatmapControls.js +0 -0
  89. /package/dist/{components → cjs/components}/matrixplot/Matrixplot.js +0 -0
  90. /package/dist/{components → cjs/components}/matrixplot/MatrixplotControls.js +0 -0
  91. /package/dist/{components → cjs/components}/obsm-list/ObsmList.js +0 -0
  92. /package/dist/{components → cjs/components}/scatterplot/ScatterplotControls.js +0 -0
  93. /package/dist/{components → cjs/components}/violin/ViolinControls.js +0 -0
  94. /package/dist/{constants → cjs/constants}/colorscales.js +0 -0
  95. /package/dist/{constants → cjs/constants}/constants.js +0 -0
  96. /package/dist/{index.js → cjs/index.js} +0 -0
  97. /package/dist/{utils → cjs/utils}/Filter.js +0 -0
  98. /package/dist/{utils → cjs/utils}/errors.js +0 -0
  99. /package/dist/{utils → cjs/utils}/search.js +0 -0
@@ -0,0 +1,123 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { faChevronDown } from "@fortawesome/free-solid-svg-icons";
3
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
4
+ import _ from "lodash";
5
+ import { ButtonGroup, Dropdown, Form } from "react-bootstrap";
6
+ import { PSEUDOSPATIAL_CATEGORICAL_MODES as MODES, PSEUDOSPATIAL_PLOT_TYPES as PLOT_TYPES } from "../../constants/constants";
7
+ import { useDataset, useDatasetDispatch } from "../../context/DatasetContext";
8
+ import { useFetch } from "../../utils/requests";
9
+ function CategoricalMode() {
10
+ const dataset = useDataset();
11
+ const dispatch = useDatasetDispatch();
12
+ const modeList = _.map(MODES, (m, key) => /*#__PURE__*/React.createElement(Dropdown.Item, {
13
+ key: key,
14
+ active: dataset.pseudospatial.categoricalMode === m,
15
+ onClick: () => {
16
+ dispatch({
17
+ type: "set.pseudospatial.categoricalMode",
18
+ categoricalMode: m.value
19
+ });
20
+ }
21
+ }, _.capitalize(m.name)));
22
+ const mode = _.find(MODES, {
23
+ value: dataset.pseudospatial.categoricalMode
24
+ });
25
+ return /*#__PURE__*/React.createElement(Dropdown, null, /*#__PURE__*/React.createElement(Dropdown.Toggle, {
26
+ variant: "light"
27
+ }, _.capitalize(mode.name)), /*#__PURE__*/React.createElement(Dropdown.Menu, null, modeList));
28
+ }
29
+ function MaskSet() {
30
+ const ENDPOINT = "masks";
31
+ const dataset = useDataset();
32
+ const dispatch = useDatasetDispatch();
33
+ const [maskSets, setMaskSets] = useState(null);
34
+ const [params, setParams] = useState({
35
+ url: dataset.url
36
+ });
37
+ useEffect(() => {
38
+ setParams(p => {
39
+ return {
40
+ ...p,
41
+ url: dataset.url
42
+ };
43
+ });
44
+ }, [dataset.url]);
45
+ const {
46
+ fetchedData,
47
+ isPending,
48
+ serverError
49
+ } = useFetch(ENDPOINT, params, {
50
+ refetchOnMount: false
51
+ });
52
+ useEffect(() => {
53
+ if (!isPending && !serverError) {
54
+ setMaskSets(fetchedData);
55
+ }
56
+ }, [fetchedData, isPending, serverError]);
57
+ const maskSetList = _.map(maskSets, (ms, key) => /*#__PURE__*/React.createElement(Dropdown.Item, {
58
+ key: key,
59
+ active: dataset.pseudospatial.maskSet === key,
60
+ onClick: () => {
61
+ dispatch({
62
+ type: "set.pseudospatial.maskSet",
63
+ maskSet: key
64
+ });
65
+ }
66
+ }, _.capitalize(key)));
67
+ const handleMaskChange = mask => {
68
+ let newMasks = dataset.pseudospatial.maskValues || maskSets?.[dataset.pseudospatial?.maskSet];
69
+ newMasks = newMasks.includes(mask) ? newMasks.filter(m => m !== mask) : [...newMasks, mask];
70
+ if (!_.difference(maskSets?.[dataset.pseudospatial?.maskSet], newMasks).length) {
71
+ newMasks = null;
72
+ }
73
+ dispatch({
74
+ type: "set.pseudospatial.maskValues",
75
+ maskValues: newMasks
76
+ });
77
+ };
78
+ const toggleMasks = () => {
79
+ if (!dataset.pseudospatial.maskValues || dataset.pseudospatial.maskValues?.length === maskSets?.[dataset.pseudospatial?.maskSet]?.length) {
80
+ dispatch({
81
+ type: "set.pseudospatial.maskValues",
82
+ maskValues: []
83
+ });
84
+ } else {
85
+ dispatch({
86
+ type: "set.pseudospatial.maskValues",
87
+ maskValues: null
88
+ });
89
+ }
90
+ };
91
+ const masksList = _.map(maskSets?.[dataset.pseudospatial?.maskSet], mask => /*#__PURE__*/React.createElement(Dropdown.ItemText, {
92
+ key: mask
93
+ }, /*#__PURE__*/React.createElement(Form.Check, {
94
+ type: "checkbox",
95
+ label: mask,
96
+ checked: !dataset.pseudospatial.maskValues || dataset.pseudospatial.maskValues.includes(mask),
97
+ onChange: () => handleMaskChange(mask)
98
+ })));
99
+ const nMasks = dataset.pseudospatial.maskValues ? dataset.pseudospatial.maskValues?.length : maskSets?.[dataset.pseudospatial?.maskSet]?.length || "No";
100
+ const toggleAllChecked = !dataset.pseudospatial.maskValues || dataset.pseudospatial.maskValues?.length === maskSets?.[dataset.pseudospatial?.maskSet]?.length;
101
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Dropdown, null, /*#__PURE__*/React.createElement(Dropdown.Toggle, {
102
+ variant: "light"
103
+ }, _.capitalize(dataset.pseudospatial.maskSet || "Select a mask set")), /*#__PURE__*/React.createElement(Dropdown.Menu, null, /*#__PURE__*/React.createElement(Dropdown.Header, null, "Mask set"), maskSetList)), /*#__PURE__*/React.createElement(Dropdown, null, /*#__PURE__*/React.createElement(Dropdown.Toggle, {
104
+ variant: "light"
105
+ }, nMasks, " masks selected"), /*#__PURE__*/React.createElement(Dropdown.Menu, null, /*#__PURE__*/React.createElement(Dropdown.Header, null, "Masks"), /*#__PURE__*/React.createElement(Dropdown.ItemText, {
106
+ key: "toggle-all"
107
+ }, /*#__PURE__*/React.createElement(Form.Check, {
108
+ type: "checkbox",
109
+ label: "Toggle all",
110
+ checked: toggleAllChecked,
111
+ onChange: toggleMasks
112
+ })), masksList)));
113
+ }
114
+
115
+ // @TODO: add colormap, colorbar slider
116
+ export function PseudospatialToolbar(_ref) {
117
+ let {
118
+ plotType
119
+ } = _ref;
120
+ return /*#__PURE__*/React.createElement("div", {
121
+ className: "cherita-pseudospatial-toolbar"
122
+ }, /*#__PURE__*/React.createElement(ButtonGroup, null, /*#__PURE__*/React.createElement(MaskSet, null)), /*#__PURE__*/React.createElement(ButtonGroup, null, plotType === PLOT_TYPES.CATEGORICAL && /*#__PURE__*/React.createElement(CategoricalMode, null)));
123
+ }
@@ -0,0 +1,394 @@
1
+ import React, { useEffect, useState, useMemo, useCallback, useDeferredValue, useRef } from "react";
2
+ import { ScatterplotLayer } from "@deck.gl/layers";
3
+ import { DeckGL } from "@deck.gl/react";
4
+ import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
5
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
6
+ import { ViewMode } from "@nebula.gl/edit-modes";
7
+ import { EditableGeoJsonLayer } from "@nebula.gl/layers";
8
+ import _ from "lodash";
9
+ import { Alert } from "react-bootstrap";
10
+ import { SpatialControls } from "./SpatialControls";
11
+ import { Toolbox } from "./Toolbox";
12
+ import { COLOR_ENCODINGS, OBS_TYPES, SELECTED_POLYGON_FILLCOLOR, UNSELECTED_POLYGON_FILLCOLOR } from "../../constants/constants";
13
+ import { useDataset, useDatasetDispatch } from "../../context/DatasetContext";
14
+ import { useFilteredData } from "../../context/FilterContext";
15
+ import { useZarrData } from "../../context/ZarrDataContext";
16
+ import { rgbToHex, useColor } from "../../helpers/color-helper";
17
+ import { MapHelper } from "../../helpers/map-helper";
18
+ import { Legend } from "../../utils/Legend";
19
+ import { LoadingLinear, LoadingSpinner } from "../../utils/LoadingIndicators";
20
+ import { formatNumerical } from "../../utils/string";
21
+ import { useLabelObsData } from "../../utils/zarrData";
22
+ window.deck.log.level = 1;
23
+ const INITIAL_VIEW_STATE = {
24
+ longitude: 0,
25
+ latitude: 0,
26
+ zoom: 0,
27
+ maxZoom: 16,
28
+ pitch: 0,
29
+ bearing: 0
30
+ };
31
+ export function Scatterplot(_ref) {
32
+ let {
33
+ radius = 30
34
+ } = _ref;
35
+ const dataset = useDataset();
36
+ const {
37
+ obsIndices,
38
+ valueMin,
39
+ valueMax,
40
+ slicedLength
41
+ } = useFilteredData();
42
+ const dispatch = useDatasetDispatch();
43
+ const {
44
+ getColor
45
+ } = useColor();
46
+ const deckRef = useRef(null);
47
+ const [viewState, setViewState] = useState(INITIAL_VIEW_STATE);
48
+ const [isRendering, setIsRendering] = useState(true);
49
+ const [data, setData] = useState({
50
+ ids: [],
51
+ positions: [],
52
+ values: [],
53
+ sliceValues: []
54
+ });
55
+
56
+ // EditableGeoJsonLayer
57
+ const [mode, setMode] = useState(() => ViewMode);
58
+ const [features, setFeatures] = useState({
59
+ type: "FeatureCollection",
60
+ features: dataset.polygons[dataset.selectedObsm] || []
61
+ });
62
+ const [selectedFeatureIndexes, setSelectedFeatureIndexes] = useState([]);
63
+ const {
64
+ obsmData,
65
+ xData,
66
+ obsData
67
+ } = useZarrData();
68
+ const labelObsData = useLabelObsData();
69
+ // @TODO: assert length of obsmData, xData, obsData is equal
70
+
71
+ useEffect(() => {
72
+ if (!obsmData.isPending && !obsmData.serverError) {
73
+ setIsRendering(true);
74
+ setData(d => {
75
+ return {
76
+ ...d,
77
+ positions: obsmData.data
78
+ };
79
+ });
80
+ const mapHelper = new MapHelper();
81
+ const {
82
+ latitude,
83
+ longitude,
84
+ zoom
85
+ } = mapHelper.fitBounds(obsmData.data, {
86
+ width: deckRef?.current?.deck?.width,
87
+ height: deckRef?.current?.deck?.height
88
+ });
89
+ setViewState(v => {
90
+ return {
91
+ ...v,
92
+ longitude: longitude,
93
+ latitude: latitude,
94
+ zoom: zoom
95
+ };
96
+ });
97
+ } else if (!obsmData.isPending && obsmData.serverError) {
98
+ setIsRendering(true);
99
+ setData(d => {
100
+ return {
101
+ ...d,
102
+ positions: []
103
+ };
104
+ });
105
+ }
106
+ }, [dataset.selectedObsm, obsmData.data, obsmData.isPending, obsmData.serverError]);
107
+ const getBounds = useCallback(() => {
108
+ const {
109
+ latitude,
110
+ longitude,
111
+ zoom
112
+ } = new MapHelper().fitBounds(data.positions, {
113
+ width: deckRef?.current?.deck?.width,
114
+ height: deckRef?.current?.deck?.height
115
+ });
116
+ return {
117
+ latitude,
118
+ longitude,
119
+ zoom
120
+ };
121
+ }, [data.positions]);
122
+ useEffect(() => {
123
+ if (dataset.colorEncoding === COLOR_ENCODINGS.VAR) {
124
+ setIsRendering(true);
125
+ if (!xData.isPending && !xData.serverError) {
126
+ // @TODO: add condition to check obs slicing
127
+ setData(d => {
128
+ return {
129
+ ...d,
130
+ values: xData.data
131
+ };
132
+ });
133
+ } else if (!xData.isPending && xData.serverError) {
134
+ setData(d => {
135
+ return {
136
+ ...d,
137
+ values: []
138
+ };
139
+ });
140
+ }
141
+ }
142
+ }, [dataset.colorEncoding, xData.data, xData.isPending, xData.serverError, getColor]);
143
+ useEffect(() => {
144
+ if (dataset.colorEncoding === COLOR_ENCODINGS.OBS) {
145
+ setIsRendering(true);
146
+ if (!obsData.isPending && !obsData.serverError) {
147
+ setData(d => {
148
+ return {
149
+ ...d,
150
+ values: obsData.data
151
+ };
152
+ });
153
+ } else if (!obsData.isPending && obsData.serverError) {
154
+ setData(d => {
155
+ return {
156
+ ...d,
157
+ values: []
158
+ };
159
+ });
160
+ }
161
+ } else if (dataset.colorEncoding === COLOR_ENCODINGS.VAR && dataset.sliceBy.obs) {
162
+ if (!obsData.isPending && !obsData.serverError) {
163
+ setData(d => {
164
+ return {
165
+ ...d,
166
+ sliceValues: obsData.data
167
+ };
168
+ });
169
+ } else if (!obsData.isPending && obsData.serverError) {
170
+ setData(d => {
171
+ return {
172
+ ...d,
173
+ sliceValues: []
174
+ };
175
+ });
176
+ }
177
+ }
178
+ }, [dataset.colorEncoding, obsData.data, obsData.isPending, obsData.serverError, dataset.sliceBy.obs]);
179
+ const isCategorical = useMemo(() => {
180
+ if (dataset.colorEncoding === COLOR_ENCODINGS.OBS) {
181
+ return dataset.selectedObs?.type === OBS_TYPES.CATEGORICAL || dataset.selectedObs?.type === OBS_TYPES.BOOLEAN;
182
+ } else {
183
+ return false;
184
+ }
185
+ }, [dataset.colorEncoding, dataset.selectedObs?.type]);
186
+ useEffect(() => {
187
+ dispatch({
188
+ type: "set.controls.valueRange",
189
+ valueRange: [valueMin, valueMax]
190
+ });
191
+ }, [dispatch, valueMax, valueMin]);
192
+ const {
193
+ min,
194
+ max
195
+ } = {
196
+ min: dataset.controls.range[0] * (valueMax - valueMin) + valueMin,
197
+ max: dataset.controls.range[1] * (valueMax - valueMin) + valueMin
198
+ };
199
+ const getFillColor = useCallback((_d, _ref2) => {
200
+ let {
201
+ index
202
+ } = _ref2;
203
+ const grayOut = obsIndices && !obsIndices.has(index);
204
+ return getColor({
205
+ value: (data.values[index] - min) / (max - min),
206
+ categorical: isCategorical,
207
+ grayOut: grayOut
208
+ }) || [0, 0, 0, 100];
209
+ }, [data.values, obsIndices, getColor, isCategorical, max, min]);
210
+
211
+ // @TODO: add support for pseudospatial hover to reflect in radius
212
+ const getRadius = useCallback((_d, _ref3) => {
213
+ let {
214
+ index
215
+ } = _ref3;
216
+ const grayOut = obsIndices && !obsIndices.has(index);
217
+ return grayOut ? 1 : 3;
218
+ }, [obsIndices]);
219
+ const memoizedLayers = useMemo(() => {
220
+ return [new ScatterplotLayer({
221
+ id: "cherita-layer-scatterplot",
222
+ pickable: true,
223
+ data: data.positions,
224
+ radiusScale: radius,
225
+ radiusMinPixels: 1,
226
+ getPosition: d => d,
227
+ getFillColor: getFillColor,
228
+ getRadius: getRadius,
229
+ updateTriggers: {
230
+ getFillColor: getFillColor,
231
+ getRadius: getRadius
232
+ }
233
+ }), new EditableGeoJsonLayer({
234
+ id: "cherita-layer-draw",
235
+ data: features,
236
+ mode: mode,
237
+ selectedFeatureIndexes,
238
+ onEdit: _ref4 => {
239
+ let {
240
+ updatedData,
241
+ editType,
242
+ editContext
243
+ } = _ref4;
244
+ setFeatures(updatedData);
245
+ let updatedSelectedFeatureIndexes = selectedFeatureIndexes;
246
+ if (editType === "addFeature") {
247
+ const {
248
+ featureIndexes
249
+ } = editContext;
250
+ updatedSelectedFeatureIndexes = [...selectedFeatureIndexes, ...featureIndexes];
251
+ }
252
+ setSelectedFeatureIndexes(updatedSelectedFeatureIndexes);
253
+ },
254
+ // getFillColor: POLYGON_FILLCOLOR,
255
+ _subLayerProps: {
256
+ geojson: {
257
+ getFillColor: feature => {
258
+ if (selectedFeatureIndexes.some(i => features.features[i] === feature)) {
259
+ return SELECTED_POLYGON_FILLCOLOR;
260
+ } else {
261
+ return UNSELECTED_POLYGON_FILLCOLOR;
262
+ }
263
+ }
264
+ }
265
+ }
266
+ })];
267
+ }, [data.positions, features, getFillColor, getRadius, mode, radius, selectedFeatureIndexes]);
268
+ const layers = useDeferredValue(mode === ViewMode ? memoizedLayers.reverse() : memoizedLayers); // draw scatterplot on top of polygons when in ViewMode
269
+
270
+ useEffect(() => {
271
+ if (!features?.features?.length) {
272
+ dispatch({
273
+ type: "disable.slice.polygons"
274
+ });
275
+ }
276
+ }, [dispatch, features?.features?.length]);
277
+ useEffect(() => {
278
+ dispatch({
279
+ type: "set.polygons",
280
+ obsm: dataset.selectedObsm,
281
+ polygons: features?.features || []
282
+ });
283
+ }, [dataset.selectedObsm, dispatch, features.features]);
284
+ function onLayerClick(info) {
285
+ if (mode !== ViewMode) {
286
+ // don't change selection while editing
287
+ return;
288
+ }
289
+ setSelectedFeatureIndexes(f => info.object ? info.layer.id === "cherita-layer-draw" ? [info.index] : f : []);
290
+ }
291
+ const getLabel = function (o, v) {
292
+ let isVar = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
293
+ if (isVar || o.type === OBS_TYPES.CONTINUOUS) {
294
+ return `${o.name}: ${formatNumerical(parseFloat(v))}`;
295
+ } else {
296
+ return `${o.name}: ${o.codesMap[v]}`;
297
+ }
298
+ };
299
+ const getTooltip = _ref5 => {
300
+ let {
301
+ object,
302
+ index
303
+ } = _ref5;
304
+ if (!object || object?.type === "Feature") return;
305
+ const text = [];
306
+ if (dataset.colorEncoding === COLOR_ENCODINGS.OBS && dataset.selectedObs && !_.some(dataset.labelObs, {
307
+ name: dataset.selectedObs.name
308
+ })) {
309
+ text.push(getLabel(dataset.selectedObs, obsData.data?.[index]));
310
+ }
311
+ if (dataset.colorEncoding === COLOR_ENCODINGS.VAR && dataset.selectedVar) {
312
+ text.push(getLabel(dataset.selectedVar, xData.data?.[index], true));
313
+ }
314
+ if (dataset.labelObs.length) {
315
+ text.push(..._.map(labelObsData.data, (v, k) => {
316
+ const labelObs = _.find(dataset.labelObs, o => o.name === k);
317
+ return getLabel(labelObs, v[index]);
318
+ }));
319
+ }
320
+ if (!text.length) return;
321
+ const grayOut = obsIndices && !obsIndices.has(index);
322
+ return {
323
+ text: text.length ? _.compact(text).join("\n") : null,
324
+ className: grayOut ? "tooltip-grayout" : "deck-tooltip",
325
+ style: !grayOut ? {
326
+ "border-left": `3px solid ${rgbToHex(getFillColor(null, {
327
+ index
328
+ }))}`
329
+ } : {
330
+ "border-left": "none"
331
+ }
332
+ };
333
+ };
334
+ const isPending = (isRendering || xData.isPending || obsmData.isPending) && !obsmData.isPending;
335
+ const error = dataset.selectedObsm && obsmData.serverError?.length || dataset.colorEncoding === COLOR_ENCODINGS.VAR && xData.serverError?.length || dataset.colorEncoding === COLOR_ENCODINGS.OBS && obsData.serverError?.length || dataset.labelObs.lengh && labelObsData.serverError?.length;
336
+ return /*#__PURE__*/React.createElement("div", {
337
+ className: "cherita-container-scatterplot"
338
+ }, /*#__PURE__*/React.createElement("div", {
339
+ className: "cherita-scatterplot"
340
+ }, obsmData.isPending && /*#__PURE__*/React.createElement(LoadingSpinner, {
341
+ disableShrink: true
342
+ }), isPending && /*#__PURE__*/React.createElement(LoadingLinear, null), /*#__PURE__*/React.createElement(DeckGL, {
343
+ viewState: viewState,
344
+ onViewStateChange: e => setViewState(e.viewState),
345
+ controller: {
346
+ doubleClickZoom: mode === ViewMode
347
+ },
348
+ layers: layers,
349
+ onClick: onLayerClick,
350
+ getTooltip: getTooltip,
351
+ onAfterRender: () => {
352
+ setIsRendering(false);
353
+ },
354
+ useDevicePixels: false,
355
+ getCursor: _ref6 => {
356
+ let {
357
+ isDragging
358
+ } = _ref6;
359
+ return mode !== ViewMode ? "crosshair" : isDragging ? "grabbing" : "grab";
360
+ },
361
+ ref: deckRef
362
+ }), /*#__PURE__*/React.createElement(SpatialControls, {
363
+ mode: mode,
364
+ setMode: setMode,
365
+ features: features,
366
+ setFeatures: setFeatures,
367
+ selectedFeatureIndexes: selectedFeatureIndexes,
368
+ resetBounds: () => setViewState(getBounds()),
369
+ increaseZoom: () => setViewState(v => ({
370
+ ...v,
371
+ zoom: v.zoom + 1
372
+ })),
373
+ decreaseZoom: () => setViewState(v => ({
374
+ ...v,
375
+ zoom: v.zoom - 1
376
+ }))
377
+ }), /*#__PURE__*/React.createElement("div", {
378
+ className: "cherita-spatial-footer"
379
+ }, /*#__PURE__*/React.createElement("div", {
380
+ className: "cherita-toolbox-footer"
381
+ }, error && !isPending && /*#__PURE__*/React.createElement(Alert, {
382
+ variant: "danger"
383
+ }, /*#__PURE__*/React.createElement(FontAwesomeIcon, {
384
+ icon: faTriangleExclamation
385
+ }), "\xA0Error loading data"), /*#__PURE__*/React.createElement(Toolbox, {
386
+ mode: dataset.colorEncoding === COLOR_ENCODINGS.VAR ? dataset.selectedVar.name : dataset.colorEncoding === COLOR_ENCODINGS.OBS ? dataset.selectedObs.name : null,
387
+ obsLength: parseInt(obsmData.data?.length),
388
+ slicedLength: parseInt(slicedLength)
389
+ })), /*#__PURE__*/React.createElement(Legend, {
390
+ isCategorical: isCategorical,
391
+ min: min,
392
+ max: max
393
+ }))));
394
+ }
@@ -0,0 +1,71 @@
1
+ import React, { useEffect } from "react";
2
+ import { Box, Slider, Typography } from "@mui/material";
3
+ import _ from "lodash";
4
+ import { Dropdown } from "react-bootstrap";
5
+ import { COLORSCALES } from "../../constants/colorscales";
6
+ import { COLOR_ENCODINGS, OBS_TYPES } from "../../constants/constants";
7
+ import { useDataset, useDatasetDispatch } from "../../context/DatasetContext";
8
+ export const ScatterplotControls = () => {
9
+ const dataset = useDataset();
10
+ const dispatch = useDatasetDispatch();
11
+ const [sliderValue, setSliderValue] = React.useState(dataset.controls.range || [0, 1]);
12
+ const isCategorical = dataset.colorEncoding === COLOR_ENCODINGS.OBS ? dataset.selectedObs?.type === OBS_TYPES.CATEGORICAL : false;
13
+ const colormapList = _.keys(COLORSCALES).map(key => /*#__PURE__*/React.createElement(Dropdown.Item, {
14
+ key: key,
15
+ active: dataset.controls.colorScale === key,
16
+ onClick: () => {
17
+ dispatch({
18
+ type: "set.controls.colorScale",
19
+ colorScale: key
20
+ });
21
+ }
22
+ }, key));
23
+ const valueLabelFormat = value => {
24
+ return (value * (dataset.controls.valueRange[1] - dataset.controls.valueRange[0]) + dataset.controls.valueRange[0]).toFixed(2);
25
+ };
26
+ const marks = [{
27
+ value: 0,
28
+ label: valueLabelFormat(0)
29
+ }, {
30
+ value: 1,
31
+ label: valueLabelFormat(1)
32
+ }];
33
+ const updateSlider = (_e, value) => {
34
+ setSliderValue(value);
35
+ };
36
+ const updateRange = (_e, value) => {
37
+ setSliderValue(value);
38
+ dispatch({
39
+ type: "set.controls.range",
40
+ range: sliderValue
41
+ });
42
+ };
43
+ useEffect(() => {
44
+ setSliderValue(dataset.controls.range);
45
+ }, [dataset.controls.range]);
46
+ const rangeSlider = /*#__PURE__*/React.createElement(Box, {
47
+ className: "w-100"
48
+ }, /*#__PURE__*/React.createElement(Typography, {
49
+ id: "colorscale-range",
50
+ gutterBottom: true
51
+ }, "Colorscale range"), /*#__PURE__*/React.createElement(Slider, {
52
+ "aria-labelledby": "colorscale-range",
53
+ min: 0,
54
+ max: 1,
55
+ step: 0.001,
56
+ value: sliderValue,
57
+ onChange: updateSlider,
58
+ onChangeCommitted: updateRange,
59
+ valueLabelDisplay: "auto",
60
+ getAriaValueText: valueLabelFormat,
61
+ valueLabelFormat: valueLabelFormat,
62
+ marks: marks,
63
+ disabled: isCategorical
64
+ }));
65
+ return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(Dropdown, null, /*#__PURE__*/React.createElement(Dropdown.Toggle, {
66
+ id: "dropdownColorscale",
67
+ variant: "light"
68
+ }, dataset.controls.colorScale), /*#__PURE__*/React.createElement(Dropdown.Menu, null, colormapList)), /*#__PURE__*/React.createElement("div", {
69
+ className: "m-4"
70
+ }, rangeSlider));
71
+ };