@haniffalab/cherita-react 1.3.0 → 1.4.0-dev.2025-06-09.09fbcbf5

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 (49) hide show
  1. package/dist/assets/images/plots/dotplot.svg +152 -0
  2. package/dist/assets/images/plots/heatmap.svg +193 -0
  3. package/dist/assets/images/plots/matrixplot.svg +275 -0
  4. package/dist/assets/images/plots/scatterplot.svg +198 -0
  5. package/dist/assets/images/plots/violin.svg +50 -0
  6. package/dist/cjs/components/dotplot/Dotplot.js +35 -5
  7. package/dist/cjs/components/full-page/FullPage.js +109 -50
  8. package/dist/cjs/components/full-page/PlotTypeSelector.js +57 -0
  9. package/dist/cjs/components/heatmap/Heatmap.js +35 -5
  10. package/dist/cjs/components/matrixplot/Matrixplot.js +35 -5
  11. package/dist/cjs/components/obs-list/ObsItem.js +49 -22
  12. package/dist/cjs/components/obs-list/ObsList.js +9 -5
  13. package/dist/cjs/components/scatterplot/Scatterplot.js +115 -95
  14. package/dist/cjs/components/scatterplot/SpatialControls.js +3 -3
  15. package/dist/cjs/components/search-bar/SearchInfo.js +3 -35
  16. package/dist/cjs/components/toolbar/Toolbar.js +102 -0
  17. package/dist/cjs/components/var-list/VarList.js +11 -3
  18. package/dist/cjs/components/violin/Violin.js +37 -6
  19. package/dist/cjs/constants/constants.js +14 -2
  20. package/dist/cjs/context/DatasetContext.js +2 -1
  21. package/dist/cjs/context/SettingsContext.js +77 -46
  22. package/dist/cjs/helpers/map-helper.js +2 -1
  23. package/dist/cjs/index.js +15 -21
  24. package/dist/css/cherita.css +76 -23
  25. package/dist/css/cherita.css.map +1 -1
  26. package/dist/esm/components/dotplot/Dotplot.js +36 -6
  27. package/dist/esm/components/full-page/FullPage.js +111 -50
  28. package/dist/esm/components/full-page/PlotTypeSelector.js +50 -0
  29. package/dist/esm/components/heatmap/Heatmap.js +36 -6
  30. package/dist/esm/components/matrixplot/Matrixplot.js +36 -6
  31. package/dist/esm/components/obs-list/ObsItem.js +49 -22
  32. package/dist/esm/components/obs-list/ObsList.js +9 -5
  33. package/dist/esm/components/scatterplot/Scatterplot.js +115 -95
  34. package/dist/esm/components/scatterplot/SpatialControls.js +3 -3
  35. package/dist/esm/components/search-bar/SearchInfo.js +1 -33
  36. package/dist/esm/components/toolbar/Toolbar.js +91 -0
  37. package/dist/esm/components/var-list/VarList.js +10 -5
  38. package/dist/esm/components/violin/Violin.js +39 -8
  39. package/dist/esm/constants/constants.js +13 -1
  40. package/dist/esm/context/DatasetContext.js +2 -1
  41. package/dist/esm/context/SettingsContext.js +77 -46
  42. package/dist/esm/helpers/map-helper.js +2 -1
  43. package/dist/esm/index.js +4 -4
  44. package/package.json +6 -4
  45. package/scss/cherita.scss +0 -1
  46. package/scss/components/layouts.scss +69 -1
  47. package/scss/components/plotly.scss +19 -14
  48. package/dist/cjs/components/full-page/FullPagePseudospatial.js +0 -157
  49. package/dist/esm/components/full-page/FullPagePseudospatial.js +0 -149
@@ -15,6 +15,7 @@ import { Alert } from "react-bootstrap";
15
15
  import { SpatialControls } from "./SpatialControls";
16
16
  import { Toolbox } from "./Toolbox";
17
17
  import { COLOR_ENCODINGS, OBS_TYPES, SELECTED_POLYGON_FILLCOLOR, UNSELECTED_POLYGON_FILLCOLOR } from "../../constants/constants";
18
+ import { useDataset } from "../../context/DatasetContext";
18
19
  import { useFilteredData } from "../../context/FilterContext";
19
20
  import { useSettings, useSettingsDispatch } from "../../context/SettingsContext";
20
21
  import { useZarrData } from "../../context/ZarrDataContext";
@@ -34,13 +35,16 @@ const INITIAL_VIEW_STATE = {
34
35
  bearing: 0
35
36
  };
36
37
  export function Scatterplot(_ref) {
37
- var _settings$selectedObs3, _features$features2, _obsmData$serverError, _xData$serverError, _obsData$serverError, _labelObsData$serverE, _settings$selectedVar, _settings$selectedObs4, _obsmData$data;
38
+ var _settings$selectedObs2, _settings$selectedObs5, _settings$selectedObs8, _features$features2, _obsmData$serverError, _xData$serverError, _obsData$serverError, _labelObsData$serverE, _settings$selectedVar, _settings$selectedObs9, _data$positions;
38
39
  let {
39
- radius = 30,
40
+ radius = null,
40
41
  setShowObs,
41
42
  setShowVars,
42
43
  isFullscreen = false
43
44
  } = _ref;
45
+ const {
46
+ useUnsColors
47
+ } = useDataset();
44
48
  const settings = useSettings();
45
49
  const {
46
50
  obsIndices,
@@ -55,12 +59,13 @@ export function Scatterplot(_ref) {
55
59
  const deckRef = useRef(null);
56
60
  const [viewState, setViewState] = useState(INITIAL_VIEW_STATE);
57
61
  const [isRendering, setIsRendering] = useState(true);
62
+ const [radiusScale, setRadiusScale] = useState(radius || 1);
63
+ const [isPending, setIsPending] = useState(false);
58
64
  const [data, setData] = useState({
59
- ids: [],
60
65
  positions: [],
61
- values: [],
62
- sliceValues: []
66
+ values: []
63
67
  });
68
+ const [coordsError, setCoordsError] = useState(null);
64
69
 
65
70
  // EditableGeoJsonLayer
66
71
  const [mode, setMode] = useState(() => ViewMode);
@@ -77,24 +82,62 @@ export function Scatterplot(_ref) {
77
82
  const labelObsData = useLabelObsData();
78
83
  // @TODO: assert length of obsmData, xData, obsData is equal
79
84
 
85
+ const getRadiusScale = useCallback(bounds => {
86
+ if (!!radius) return radius;
87
+ // From 28 degrees ~= 30km -> 30m radius
88
+ const lonDim = bounds[1][0] - bounds[0][0];
89
+ const latDim = bounds[1][1] - bounds[0][1];
90
+ const minDim = Math.min(lonDim, latDim);
91
+ const rs = 0.01 / minDim * 111111;
92
+ return rs;
93
+ }, [radius]);
80
94
  useEffect(() => {
81
- if (!obsmData.isPending && !obsmData.serverError) {
82
- var _deckRef$current, _deckRef$current2;
83
- setIsRendering(true);
95
+ if (obsmData.isPending || settings.colorEncoding === COLOR_ENCODINGS.VAR && xData.isPending || settings.colorEncoding === COLOR_ENCODINGS.OBS && obsData.isPending) {
96
+ setIsPending(true);
97
+ } else {
98
+ setIsPending(false);
84
99
  setData(d => {
85
- return _objectSpread(_objectSpread({}, d), {}, {
86
- positions: obsmData.data
87
- });
100
+ let values = d.values;
101
+ if (settings.colorEncoding === COLOR_ENCODINGS.VAR) {
102
+ values = !xData.serverError ? xData.data : values;
103
+ } else if (settings.colorEncoding === COLOR_ENCODINGS.OBS) {
104
+ values = !obsData.serverError ? obsData.data : values;
105
+ }
106
+ if (!obsmData.serverError && obsmData.data) {
107
+ if (obsmData.data[0].length !== 2) {
108
+ setCoordsError("Invalid coordinates. Expected 2D coordinates");
109
+ return {
110
+ positions: [],
111
+ values: []
112
+ };
113
+ }
114
+ setCoordsError(null);
115
+ return {
116
+ positions: obsmData.data,
117
+ values: values
118
+ };
119
+ }
120
+ return {
121
+ positions: d.positions,
122
+ values: values
123
+ };
88
124
  });
125
+ }
126
+ }, [obsData.data, obsData.isPending, obsData.serverError, obsmData.data, obsmData.isPending, obsmData.serverError, settings.colorEncoding, xData.data, xData.isPending, xData.serverError]);
127
+ useEffect(() => {
128
+ if (data.positions && !!data.positions.length) {
129
+ var _deckRef$current, _deckRef$current2;
89
130
  const mapHelper = new MapHelper();
90
131
  const {
91
132
  latitude,
92
133
  longitude,
93
- zoom
94
- } = mapHelper.fitBounds(obsmData.data, {
134
+ zoom,
135
+ bounds
136
+ } = mapHelper.fitBounds(data.positions, {
95
137
  width: deckRef === null || deckRef === void 0 || (_deckRef$current = deckRef.current) === null || _deckRef$current === void 0 || (_deckRef$current = _deckRef$current.deck) === null || _deckRef$current === void 0 ? void 0 : _deckRef$current.width,
96
138
  height: deckRef === null || deckRef === void 0 || (_deckRef$current2 = deckRef.current) === null || _deckRef$current2 === void 0 || (_deckRef$current2 = _deckRef$current2.deck) === null || _deckRef$current2 === void 0 ? void 0 : _deckRef$current2.height
97
139
  });
140
+ setRadiusScale(getRadiusScale(bounds));
98
141
  setViewState(v => {
99
142
  return _objectSpread(_objectSpread({}, v), {}, {
100
143
  longitude: longitude,
@@ -102,15 +145,8 @@ export function Scatterplot(_ref) {
102
145
  zoom: zoom
103
146
  });
104
147
  });
105
- } else if (!obsmData.isPending && obsmData.serverError) {
106
- setIsRendering(true);
107
- setData(d => {
108
- return _objectSpread(_objectSpread({}, d), {}, {
109
- positions: []
110
- });
111
- });
112
148
  }
113
- }, [settings.selectedObsm, obsmData.data, obsmData.isPending, obsmData.serverError]);
149
+ }, [settings.selectedObsm, getRadiusScale, obsmData.data, obsmData.isPending, obsmData.serverError, data.positions]);
114
150
  const getBounds = useCallback(() => {
115
151
  var _deckRef$current3, _deckRef$current4;
116
152
  const {
@@ -127,65 +163,47 @@ export function Scatterplot(_ref) {
127
163
  zoom
128
164
  };
129
165
  }, [data.positions]);
130
- useEffect(() => {
131
- if (settings.colorEncoding === COLOR_ENCODINGS.VAR) {
132
- setIsRendering(true);
133
- if (!xData.isPending && !xData.serverError) {
134
- // @TODO: add condition to check obs slicing
135
- setData(d => {
136
- return _objectSpread(_objectSpread({}, d), {}, {
137
- values: xData.data
138
- });
139
- });
140
- } else if (!xData.isPending && xData.serverError) {
141
- setData(d => {
142
- return _objectSpread(_objectSpread({}, d), {}, {
143
- values: []
144
- });
145
- });
146
- }
147
- }
148
- }, [settings.colorEncoding, xData.data, xData.isPending, xData.serverError, getColor]);
149
- useEffect(() => {
150
- if (settings.colorEncoding === COLOR_ENCODINGS.OBS) {
151
- setIsRendering(true);
152
- if (!obsData.isPending && !obsData.serverError) {
153
- setData(d => {
154
- return _objectSpread(_objectSpread({}, d), {}, {
155
- values: obsData.data
156
- });
157
- });
158
- } else if (!obsData.isPending && obsData.serverError) {
159
- setData(d => {
160
- return _objectSpread(_objectSpread({}, d), {}, {
161
- values: []
162
- });
163
- });
164
- }
165
- } else if (settings.colorEncoding === COLOR_ENCODINGS.VAR && settings.sliceBy.obs) {
166
- if (!obsData.isPending && !obsData.serverError) {
167
- setData(d => {
168
- return _objectSpread(_objectSpread({}, d), {}, {
169
- sliceValues: obsData.data
170
- });
171
- });
172
- } else if (!obsData.isPending && obsData.serverError) {
173
- setData(d => {
174
- return _objectSpread(_objectSpread({}, d), {}, {
175
- sliceValues: []
176
- });
177
- });
178
- }
166
+
167
+ // Make stable references for getOriginalIndex and sortedIndexMap
168
+ const identityGetOriginalIndex = useCallback(i => i, []);
169
+ const identitySortedIndexMap = useMemo(() => ({
170
+ get: key => key
171
+ }), []);
172
+ const {
173
+ sortedData,
174
+ getOriginalIndex,
175
+ sortedIndexMap
176
+ } = useMemo(() => {
177
+ var _settings$selectedObs;
178
+ if ((settings.colorEncoding === COLOR_ENCODINGS.VAR || settings.colorEncoding === COLOR_ENCODINGS.OBS && ((_settings$selectedObs = settings.selectedObs) === null || _settings$selectedObs === void 0 ? void 0 : _settings$selectedObs.type) === OBS_TYPES.CONTINUOUS) && data.positions && data.values && data.positions.length === data.values.length) {
179
+ const sortedIndices = _.map(data.values, (_v, i) => i).sort((a, b) => data.values[a] - data.values[b]);
180
+ const sortedIndexMap = new Map(_.map(sortedIndices, (originalIndex, sortedIndex) => [originalIndex, sortedIndex]));
181
+ return {
182
+ sortedData: _.mapValues(data, (v, _k) => {
183
+ return v ? _.at(v, sortedIndices) : v;
184
+ }),
185
+ getOriginalIndex: i => sortedIndices[i],
186
+ sortedIndexMap: sortedIndexMap
187
+ };
179
188
  }
180
- }, [settings.colorEncoding, obsData.data, obsData.isPending, obsData.serverError, settings.sliceBy.obs]);
189
+ return {
190
+ sortedData: data,
191
+ getOriginalIndex: identityGetOriginalIndex,
192
+ // return original index
193
+ sortedIndexMap: identitySortedIndexMap // return original index
194
+ };
195
+ }, [data, identityGetOriginalIndex, identitySortedIndexMap, settings.colorEncoding, (_settings$selectedObs2 = settings.selectedObs) === null || _settings$selectedObs2 === void 0 ? void 0 : _settings$selectedObs2.type]);
196
+ const sortedObsIndices = useMemo(() => {
197
+ return obsIndices ? new Set(Array.from(obsIndices, i => sortedIndexMap.get(i))) : obsIndices;
198
+ }, [obsIndices, sortedIndexMap]);
181
199
  const isCategorical = useMemo(() => {
182
200
  if (settings.colorEncoding === COLOR_ENCODINGS.OBS) {
183
- var _settings$selectedObs, _settings$selectedObs2;
184
- return ((_settings$selectedObs = settings.selectedObs) === null || _settings$selectedObs === void 0 ? void 0 : _settings$selectedObs.type) === OBS_TYPES.CATEGORICAL || ((_settings$selectedObs2 = settings.selectedObs) === null || _settings$selectedObs2 === void 0 ? void 0 : _settings$selectedObs2.type) === OBS_TYPES.BOOLEAN;
201
+ var _settings$selectedObs3, _settings$selectedObs4;
202
+ return ((_settings$selectedObs3 = settings.selectedObs) === null || _settings$selectedObs3 === void 0 ? void 0 : _settings$selectedObs3.type) === OBS_TYPES.CATEGORICAL || ((_settings$selectedObs4 = settings.selectedObs) === null || _settings$selectedObs4 === void 0 ? void 0 : _settings$selectedObs4.type) === OBS_TYPES.BOOLEAN;
185
203
  } else {
186
204
  return false;
187
205
  }
188
- }, [settings.colorEncoding, (_settings$selectedObs3 = settings.selectedObs) === null || _settings$selectedObs3 === void 0 ? void 0 : _settings$selectedObs3.type]);
206
+ }, [settings.colorEncoding, (_settings$selectedObs5 = settings.selectedObs) === null || _settings$selectedObs5 === void 0 ? void 0 : _settings$selectedObs5.type]);
189
207
  useEffect(() => {
190
208
  dispatch({
191
209
  type: "set.controls.valueRange",
@@ -200,31 +218,34 @@ export function Scatterplot(_ref) {
200
218
  max: settings.controls.range[1] * (valueMax - valueMin) + valueMin
201
219
  };
202
220
  const getFillColor = useCallback((_d, _ref2) => {
221
+ var _settings$selectedObs6, _settings$selectedObs7;
203
222
  let {
204
223
  index
205
224
  } = _ref2;
206
- const grayOut = obsIndices && !obsIndices.has(index);
207
- return getColor({
208
- value: (data.values[index] - min) / (max - min),
225
+ const grayOut = isPending || sortedObsIndices && !sortedObsIndices.has(index);
226
+ return getColor(_objectSpread({
227
+ value: (sortedData.values[index] - min) / (max - min),
209
228
  categorical: isCategorical,
210
229
  grayOut: grayOut
211
- }) || [0, 0, 0, 100];
212
- }, [data.values, obsIndices, getColor, isCategorical, max, min]);
230
+ }, useUnsColors && settings.colorEncoding === COLOR_ENCODINGS.OBS && (_settings$selectedObs6 = settings.selectedObs) !== null && _settings$selectedObs6 !== void 0 && _settings$selectedObs6.colors ? {
231
+ colorscale: (_settings$selectedObs7 = settings.selectedObs) === null || _settings$selectedObs7 === void 0 ? void 0 : _settings$selectedObs7.colors
232
+ } : {})) || [0, 0, 0, 100];
233
+ }, [isPending, sortedObsIndices, getColor, sortedData.values, min, max, isCategorical, useUnsColors, settings.colorEncoding, (_settings$selectedObs8 = settings.selectedObs) === null || _settings$selectedObs8 === void 0 ? void 0 : _settings$selectedObs8.colors]);
213
234
 
214
235
  // @TODO: add support for pseudospatial hover to reflect in radius
215
236
  const getRadius = useCallback((_d, _ref3) => {
216
237
  let {
217
238
  index
218
239
  } = _ref3;
219
- const grayOut = obsIndices && !obsIndices.has(index);
240
+ const grayOut = sortedObsIndices && !sortedObsIndices.has(index);
220
241
  return grayOut ? 1 : 3;
221
- }, [obsIndices]);
242
+ }, [sortedObsIndices]);
222
243
  const memoizedLayers = useMemo(() => {
223
244
  return [new ScatterplotLayer({
224
245
  id: "cherita-layer-scatterplot",
225
246
  pickable: true,
226
- data: data.positions,
227
- radiusScale: radius,
247
+ data: sortedData.positions,
248
+ radiusScale: radiusScale,
228
249
  radiusMinPixels: 1,
229
250
  getPosition: d => d,
230
251
  getFillColor: getFillColor,
@@ -267,7 +288,7 @@ export function Scatterplot(_ref) {
267
288
  }
268
289
  }
269
290
  })];
270
- }, [data.positions, features, getFillColor, getRadius, mode, radius, selectedFeatureIndexes]);
291
+ }, [sortedData.positions, features, getFillColor, getRadius, mode, radiusScale, selectedFeatureIndexes]);
271
292
  const layers = useDeferredValue(mode === ViewMode ? memoizedLayers.reverse() : memoizedLayers); // draw scatterplot on top of polygons when in ViewMode
272
293
 
273
294
  useEffect(() => {
@@ -310,21 +331,21 @@ export function Scatterplot(_ref) {
310
331
  if (settings.colorEncoding === COLOR_ENCODINGS.OBS && settings.selectedObs && !_.some(settings.labelObs, {
311
332
  name: settings.selectedObs.name
312
333
  })) {
313
- var _obsData$data;
314
- text.push(getLabel(settings.selectedObs, (_obsData$data = obsData.data) === null || _obsData$data === void 0 ? void 0 : _obsData$data[index]));
334
+ var _data$values;
335
+ text.push(getLabel(settings.selectedObs, (_data$values = data.values) === null || _data$values === void 0 ? void 0 : _data$values[getOriginalIndex(index)]));
315
336
  }
316
337
  if (settings.colorEncoding === COLOR_ENCODINGS.VAR && settings.selectedVar) {
317
- var _xData$data;
318
- text.push(getLabel(settings.selectedVar, (_xData$data = xData.data) === null || _xData$data === void 0 ? void 0 : _xData$data[index], true));
338
+ var _data$values2;
339
+ text.push(getLabel(settings.selectedVar, (_data$values2 = data.values) === null || _data$values2 === void 0 ? void 0 : _data$values2[getOriginalIndex(index)], true));
319
340
  }
320
341
  if (settings.labelObs.length) {
321
342
  text.push(..._.map(labelObsData.data, (v, k) => {
322
343
  const labelObs = _.find(settings.labelObs, o => o.name === k);
323
- return getLabel(labelObs, v[index]);
344
+ return getLabel(labelObs, v[getOriginalIndex(index)]);
324
345
  }));
325
346
  }
326
347
  if (!text.length) return;
327
- const grayOut = obsIndices && !obsIndices.has(index);
348
+ const grayOut = sortedObsIndices && !sortedObsIndices.has(index);
328
349
  return {
329
350
  text: text.length ? _.compact(text).join("\n") : null,
330
351
  className: grayOut ? "tooltip-grayout" : "deck-tooltip",
@@ -337,8 +358,7 @@ export function Scatterplot(_ref) {
337
358
  }
338
359
  };
339
360
  };
340
- const isPending = (isRendering || xData.isPending || obsmData.isPending) && !obsmData.isPending;
341
- const error = settings.selectedObsm && ((_obsmData$serverError = obsmData.serverError) === null || _obsmData$serverError === void 0 ? void 0 : _obsmData$serverError.length) || settings.colorEncoding === COLOR_ENCODINGS.VAR && ((_xData$serverError = xData.serverError) === null || _xData$serverError === void 0 ? void 0 : _xData$serverError.length) || settings.colorEncoding === COLOR_ENCODINGS.OBS && ((_obsData$serverError = obsData.serverError) === null || _obsData$serverError === void 0 ? void 0 : _obsData$serverError.length) || settings.labelObs.lengh && ((_labelObsData$serverE = labelObsData.serverError) === null || _labelObsData$serverE === void 0 ? void 0 : _labelObsData$serverE.length);
361
+ const error = settings.selectedObsm && ((_obsmData$serverError = obsmData.serverError) === null || _obsmData$serverError === void 0 ? void 0 : _obsmData$serverError.length) || settings.colorEncoding === COLOR_ENCODINGS.VAR && ((_xData$serverError = xData.serverError) === null || _xData$serverError === void 0 ? void 0 : _xData$serverError.length) || settings.colorEncoding === COLOR_ENCODINGS.OBS && ((_obsData$serverError = obsData.serverError) === null || _obsData$serverError === void 0 ? void 0 : _obsData$serverError.length) || settings.labelObs.length && ((_labelObsData$serverE = labelObsData.serverError) === null || _labelObsData$serverE === void 0 ? void 0 : _labelObsData$serverE.length) || coordsError;
342
362
  return /*#__PURE__*/React.createElement("div", {
343
363
  className: "cherita-container-scatterplot"
344
364
  }, /*#__PURE__*/React.createElement("div", {
@@ -385,13 +405,13 @@ export function Scatterplot(_ref) {
385
405
  className: "cherita-spatial-footer"
386
406
  }, /*#__PURE__*/React.createElement("div", {
387
407
  className: "cherita-toolbox-footer"
388
- }, error && !isPending && /*#__PURE__*/React.createElement(Alert, {
408
+ }, !!error && !isRendering && /*#__PURE__*/React.createElement(Alert, {
389
409
  variant: "danger"
390
410
  }, /*#__PURE__*/React.createElement(FontAwesomeIcon, {
391
411
  icon: faTriangleExclamation
392
412
  }), "\xA0Error loading data"), /*#__PURE__*/React.createElement(Toolbox, {
393
- 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 ? (_settings$selectedObs4 = settings.selectedObs) === null || _settings$selectedObs4 === void 0 ? void 0 : _settings$selectedObs4.name : null,
394
- obsLength: parseInt((_obsmData$data = obsmData.data) === null || _obsmData$data === void 0 ? void 0 : _obsmData$data.length),
413
+ 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 ? (_settings$selectedObs9 = settings.selectedObs) === null || _settings$selectedObs9 === void 0 ? void 0 : _settings$selectedObs9.name : null,
414
+ obsLength: parseInt((_data$positions = data.positions) === null || _data$positions === void 0 ? void 0 : _data$positions.length),
395
415
  slicedLength: parseInt(slicedLength)
396
416
  })), /*#__PURE__*/React.createElement(Legend, {
397
417
  isCategorical: isCategorical,
@@ -8,9 +8,9 @@ import { OverlayTrigger, Tooltip } from "react-bootstrap";
8
8
  import Button from "react-bootstrap/Button";
9
9
  import ButtonGroup from "react-bootstrap/ButtonGroup";
10
10
  import Dropdown from "react-bootstrap/Dropdown";
11
- import { useDataset } from "../../context/DatasetContext";
12
11
  import { OffcanvasControls } from "../offcanvas";
13
12
  import { ScatterplotControls } from "./ScatterplotControls";
13
+ import { BREAKPOINTS } from "../../constants/constants";
14
14
  import { useSettings, useSettingsDispatch } from "../../context/SettingsContext";
15
15
  export function SpatialControls(_ref) {
16
16
  var _features$features;
@@ -32,8 +32,8 @@ export function SpatialControls(_ref) {
32
32
  const [showControls, setShowControls] = useState(false);
33
33
  const handleCloseControls = () => setShowControls(false);
34
34
  const handleShowControls = () => setShowControls(true);
35
- const LgBreakpoint = useMediaQuery("(max-width: 991.98px)");
36
- const XlBreakpoint = useMediaQuery("(max-width: 1199.98px)");
35
+ const LgBreakpoint = useMediaQuery(BREAKPOINTS.LG);
36
+ const XlBreakpoint = useMediaQuery(BREAKPOINTS.XL);
37
37
  const showObsBtn = isFullscreen ? LgBreakpoint : true;
38
38
  const showVarsBtn = isFullscreen ? XlBreakpoint : true;
39
39
  const onSelect = (eventKey, event) => {
@@ -13,6 +13,7 @@ import { useDataset } from "../../context/DatasetContext";
13
13
  import { useSettings, useSettingsDispatch } from "../../context/SettingsContext";
14
14
  import { useFetch } from "../../utils/requests";
15
15
  import { VarDiseaseInfo } from "../var-list/VarItem";
16
+ import { sortMeans, useVarMean } from "../var-list/VarList";
16
17
  export function VarInfo(_ref) {
17
18
  let {
18
19
  varItem
@@ -43,39 +44,6 @@ export function VarInfo(_ref) {
43
44
  data: fetchedData
44
45
  })));
45
46
  }
46
- const useVarMean = function (varKeys) {
47
- let enabled = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
48
- const ENDPOINT = "matrix/mean";
49
- const dataset = useDataset();
50
- const [params, setParams] = useState({
51
- url: dataset.url,
52
- varKeys: _.map(varKeys, v => v.isSet ? {
53
- name: v.name,
54
- indices: v.vars.map(v => v.index)
55
- } : v.index),
56
- // obsIndices:
57
- varNamesCol: dataset.varNamesCol
58
- });
59
- useEffect(() => {
60
- setParams(p => {
61
- return _objectSpread(_objectSpread({}, p), {}, {
62
- varKeys: _.map(varKeys, v => v.isSet ? {
63
- name: v.name,
64
- indices: v.vars.map(v => v.index)
65
- } : v.index)
66
- });
67
- });
68
- }, [varKeys]);
69
- return useFetch(ENDPOINT, params, {
70
- enabled: enabled,
71
- refetchOnMount: false
72
- });
73
- };
74
-
75
- // ensure nulls are lowest values
76
- const sortMeans = (i, means) => {
77
- return means[i.name] || _.min(_.values(means)) - 1;
78
- };
79
47
  export function DiseaseInfo(_ref2) {
80
48
  let {
81
49
  disease,
@@ -0,0 +1,91 @@
1
+ import React from "react";
2
+ import { faList, faSearch, faSliders } from "@fortawesome/free-solid-svg-icons";
3
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
4
+ import { Container, Nav, Navbar } from "react-bootstrap";
5
+ export const Toolbar = _ref => {
6
+ let {
7
+ showObsBtn = true,
8
+ showVarsBtn = true,
9
+ showCtrlsBtn = true,
10
+ setShowObs,
11
+ setShowVars,
12
+ setShowControls
13
+ } = _ref;
14
+ return /*#__PURE__*/React.createElement(Navbar, {
15
+ expand: "md",
16
+ bg: "primary",
17
+ variant: "dark",
18
+ className: "cherita-navbar"
19
+ }, /*#__PURE__*/React.createElement(Container, {
20
+ fluid: true
21
+ }, /*#__PURE__*/React.createElement(Navbar.Toggle, {
22
+ "aria-controls": "navbarScroll"
23
+ }), /*#__PURE__*/React.createElement(Navbar.Collapse, {
24
+ id: "navbarScroll"
25
+ }, /*#__PURE__*/React.createElement(Nav, {
26
+ navbarScroll: true
27
+ }, showObsBtn && /*#__PURE__*/React.createElement(Nav.Item, {
28
+ className: "me-2"
29
+ }, /*#__PURE__*/React.createElement(Nav.Link, {
30
+ onClick: () => setShowObs(true)
31
+ }, /*#__PURE__*/React.createElement(FontAwesomeIcon, {
32
+ icon: faList,
33
+ className: "me-2"
34
+ }), "Explore Categories")), showVarsBtn && /*#__PURE__*/React.createElement(Nav.Item, {
35
+ className: "me-2"
36
+ }, /*#__PURE__*/React.createElement(Nav.Link, {
37
+ onClick: () => setShowVars(true)
38
+ }, /*#__PURE__*/React.createElement(FontAwesomeIcon, {
39
+ icon: faSearch,
40
+ className: "me-2"
41
+ }), "Search Genes")), showCtrlsBtn && /*#__PURE__*/React.createElement(Nav.Item, {
42
+ className: "me-2"
43
+ }, /*#__PURE__*/React.createElement(Nav.Link, {
44
+ onClick: () => setShowControls(true)
45
+ }, /*#__PURE__*/React.createElement(FontAwesomeIcon, {
46
+ icon: faSliders,
47
+ className: "me-2"
48
+ }), "Controls"))))));
49
+ };
50
+ export const ObsPlotlyToolbar = _ref2 => {
51
+ let {
52
+ onClick
53
+ } = _ref2;
54
+ return {
55
+ name: "Categories",
56
+ icon: {
57
+ width: 512,
58
+ height: 512,
59
+ path: faList.icon[4]
60
+ },
61
+ click: onClick
62
+ };
63
+ };
64
+ export const VarPlotlyToolbar = _ref3 => {
65
+ let {
66
+ onClick
67
+ } = _ref3;
68
+ return {
69
+ name: "Features",
70
+ icon: {
71
+ width: 512,
72
+ height: 512,
73
+ path: faSearch.icon[4]
74
+ },
75
+ click: onClick
76
+ };
77
+ };
78
+ export const ControlsPlotlyToolbar = _ref4 => {
79
+ let {
80
+ onClick
81
+ } = _ref4;
82
+ return {
83
+ name: "Controls",
84
+ icon: {
85
+ width: 512,
86
+ height: 512,
87
+ path: faSliders.icon[4]
88
+ },
89
+ click: onClick
90
+ };
91
+ };
@@ -14,20 +14,24 @@ import { VarListToolbar } from "./VarListToolbar";
14
14
  import { VarSet } from "./VarSet";
15
15
  import { SELECTION_MODES, VAR_SORT } from "../../constants/constants";
16
16
  import { useDataset } from "../../context/DatasetContext";
17
+ import { useFilteredData } from "../../context/FilterContext";
17
18
  import { useSettings, useSettingsDispatch } from "../../context/SettingsContext";
18
19
  import { LoadingSpinner } from "../../utils/LoadingIndicators";
19
20
  import { useFetch } from "../../utils/requests";
20
- const useVarMean = function (varKeys) {
21
+ export const useVarMean = function (varKeys) {
21
22
  let enabled = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
22
23
  const ENDPOINT = "matrix/mean";
23
24
  const dataset = useDataset();
25
+ const {
26
+ obsIndices
27
+ } = useFilteredData();
24
28
  const [params, setParams] = useState({
25
29
  url: dataset.url,
26
30
  varKeys: _.map(varKeys, v => v.isSet ? {
27
31
  name: v.name,
28
32
  indices: v.vars.map(v => v.index)
29
33
  } : v.index),
30
- // obsIndices:
34
+ obsIndices: obsIndices,
31
35
  varNamesCol: dataset.varNamesCol
32
36
  });
33
37
  useEffect(() => {
@@ -36,10 +40,11 @@ const useVarMean = function (varKeys) {
36
40
  varKeys: _.map(varKeys, v => v.isSet ? {
37
41
  name: v.name,
38
42
  indices: v.vars.map(v => v.index)
39
- } : v.index)
43
+ } : v.index),
44
+ obsIndices: obsIndices
40
45
  });
41
46
  });
42
- }, [varKeys]);
47
+ }, [obsIndices, varKeys]);
43
48
  return useFetch(ENDPOINT, params, {
44
49
  enabled: enabled,
45
50
  refetchOnMount: false
@@ -47,7 +52,7 @@ const useVarMean = function (varKeys) {
47
52
  };
48
53
 
49
54
  // ensure nulls are lowest values
50
- const sortMeans = (i, means) => {
55
+ export const sortMeans = (i, means) => {
51
56
  return means[i.name] || _.min(_.values(means)) - 1;
52
57
  };
53
58
  export function VarNamesList(_ref) {
@@ -7,18 +7,25 @@ import React, { useEffect, useState } from "react";
7
7
  import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
8
8
  import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
9
9
  import _ from "lodash";
10
- import { Alert, OverlayTrigger, Tooltip } from "react-bootstrap";
10
+ import { Alert, Button, OverlayTrigger, Tooltip } from "react-bootstrap";
11
11
  import Plot from "react-plotly.js";
12
- import { VIOLIN_MODES } from "../../constants/constants";
12
+ import { PLOTLY_MODEBAR_BUTTONS, VIOLIN_MODES } from "../../constants/constants";
13
13
  import { useDataset } from "../../context/DatasetContext";
14
14
  import { useFilteredData } from "../../context/FilterContext";
15
15
  import { useSettings } from "../../context/SettingsContext";
16
16
  import { LoadingSpinner } from "../../utils/LoadingIndicators";
17
17
  import { useDebouncedFetch } from "../../utils/requests";
18
+ import { ControlsPlotlyToolbar, ObsPlotlyToolbar, VarPlotlyToolbar } from "../toolbar/Toolbar";
18
19
  export function Violin(_ref) {
19
20
  var _settings$selectedVar, _settings$selectedVar2, _settings$selectedVar3, _settings$selectedVar4, _settings$selectedObs, _settings$selectedObs2, _settings$selectedObs3;
20
21
  let {
21
- mode = VIOLIN_MODES.MULTIKEY
22
+ mode = VIOLIN_MODES.MULTIKEY,
23
+ showObsBtn = false,
24
+ showVarsBtn = false,
25
+ showCtrlsBtn = false,
26
+ setShowObs,
27
+ setShowVars,
28
+ setShowControls
22
29
  } = _ref;
23
30
  const ENDPOINT = "violin";
24
31
  const dataset = useDataset();
@@ -117,17 +124,29 @@ export function Violin(_ref) {
117
124
  setLayout(fetchedData.layout);
118
125
  }
119
126
  }, [fetchedData, hasSelections, isPending, serverError]);
127
+ const customModeBarButtons = _.compact([showObsBtn && ObsPlotlyToolbar({
128
+ onClick: setShowObs
129
+ }), showVarsBtn && VarPlotlyToolbar({
130
+ onClick: setShowVars
131
+ }), showCtrlsBtn && ControlsPlotlyToolbar({
132
+ onClick: setShowControls
133
+ })]);
134
+ const modeBarButtons = customModeBarButtons.length ? [customModeBarButtons, PLOTLY_MODEBAR_BUTTONS] : [PLOTLY_MODEBAR_BUTTONS];
120
135
  if (!serverError) {
121
136
  if (hasSelections) {
122
137
  return /*#__PURE__*/React.createElement("div", {
123
- className: "cherita-violin position-relative"
138
+ className: "cherita-plot cherita-violin position-relative"
124
139
  }, isPending && /*#__PURE__*/React.createElement(LoadingSpinner, null), /*#__PURE__*/React.createElement(Plot, {
125
140
  data: data,
126
141
  layout: layout,
127
142
  useResizeHandler: true,
128
143
  style: {
129
- maxWidth: "100%",
130
- maxHeight: "100%"
144
+ width: "100%",
145
+ height: "100%"
146
+ },
147
+ config: {
148
+ displaylogo: false,
149
+ modeBarButtons: modeBarButtons
131
150
  }
132
151
  }), (fetchedData === null || fetchedData === void 0 ? void 0 : fetchedData.resampled) && /*#__PURE__*/React.createElement(Alert, {
133
152
  variant: "warning"
@@ -142,9 +161,21 @@ export function Violin(_ref) {
142
161
  className: "cherita-violin"
143
162
  }, mode === VIOLIN_MODES.MULTIKEY && /*#__PURE__*/React.createElement(Alert, {
144
163
  variant: "light"
145
- }, "Select features"), mode === VIOLIN_MODES.GROUPBY && /*#__PURE__*/React.createElement(Alert, {
164
+ }, "Select", " ", showVarsBtn ? /*#__PURE__*/React.createElement(Button, {
165
+ variant: "link",
166
+ className: "border-0 p-0 align-baseline",
167
+ onClick: setShowVars
168
+ }, "features") : "features"), mode === VIOLIN_MODES.GROUPBY && /*#__PURE__*/React.createElement(Alert, {
146
169
  variant: "light"
147
- }, "Select categories and a feature"));
170
+ }, "Select", " ", showObsBtn ? /*#__PURE__*/React.createElement(Button, {
171
+ variant: "link",
172
+ className: "border-0 p-0 align-baseline",
173
+ onClick: setShowObs
174
+ }, "categories") : "categories", " ", "and a", " ", showVarsBtn ? /*#__PURE__*/React.createElement(Button, {
175
+ variant: "link",
176
+ className: "border-0 p-0 align-baseline",
177
+ onClick: setShowVars
178
+ }, "feature") : "feature"));
148
179
  } else {
149
180
  return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(Alert, {
150
181
  variant: "danger"
@@ -1,4 +1,11 @@
1
1
  export const LOCAL_STORAGE_KEY = "CHERITA";
2
+ export const PLOT_TYPES = {
3
+ SCATTERPLOT: "scatterplot",
4
+ DOTPLOT: "dotplot",
5
+ HEATMAP: "heatmap",
6
+ MATRIXPLOT: "matrixplot",
7
+ VIOLINPLOT: "violinplot"
8
+ };
2
9
  export const COLOR_ENCODINGS = {
3
10
  VAR: "var",
4
11
  OBS: "obs"
@@ -85,4 +92,9 @@ export const PSEUDOSPATIAL_CATEGORICAL_MODES = {
85
92
 
86
93
  // `default` cols to be shown out of accordion, at top of obslist
87
94
  // default values from cellxgene schema
88
- export const DEFAULT_OBS_GROUP = ["assay", "cell_type", "development_stage", "disease", "donor_id", "organism", "self_reported_ethnicity", "sex", "suspension_type", "tissue", "tissue_type"];
95
+ export const DEFAULT_OBS_GROUP = ["assay", "cell_type", "development_stage", "disease", "donor_id", "organism", "self_reported_ethnicity", "sex", "suspension_type", "tissue", "tissue_type"];
96
+ export const PLOTLY_MODEBAR_BUTTONS = ["toImage", "zoom2d", "pan2d", "zoomIn2d", "zoomOut2d", "autoScale2d", "resetScale2d"];
97
+ export const BREAKPOINTS = {
98
+ LG: "(max-width: 991.98px)",
99
+ XL: "(max-width: 1199.98px)"
100
+ };