@haniffalab/cherita-react 1.4.0 → 1.4.1-dev.2025-06-30.e26168b5

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 (39) hide show
  1. package/dist/cjs/components/full-page/FullPage.js +10 -42
  2. package/dist/cjs/components/full-page/PlotTypeSelector.js +4 -4
  3. package/dist/cjs/components/obs-list/ObsItem.js +27 -13
  4. package/dist/cjs/components/obs-list/ObsList.js +6 -2
  5. package/dist/cjs/components/obsm-list/ObsmList.js +30 -6
  6. package/dist/cjs/components/offcanvas/index.js +9 -4
  7. package/dist/cjs/components/scatterplot/Scatterplot.js +1 -1
  8. package/dist/cjs/components/search-bar/SearchBar.js +10 -1
  9. package/dist/cjs/components/search-bar/SearchResults.js +2 -2
  10. package/dist/cjs/components/var-list/VarItem.js +26 -25
  11. package/dist/cjs/components/var-list/VarList.js +3 -5
  12. package/dist/cjs/components/violin/Violin.js +15 -5
  13. package/dist/cjs/constants/constants.js +5 -2
  14. package/dist/cjs/context/DatasetContext.js +1 -1
  15. package/dist/cjs/context/SettingsContext.js +19 -2
  16. package/dist/cjs/utils/VirtualizedList.js +2 -2
  17. package/dist/css/cherita.css +85 -37
  18. package/dist/css/cherita.css.map +1 -1
  19. package/dist/esm/components/full-page/FullPage.js +11 -43
  20. package/dist/esm/components/full-page/PlotTypeSelector.js +4 -4
  21. package/dist/esm/components/obs-list/ObsItem.js +27 -13
  22. package/dist/esm/components/obs-list/ObsList.js +6 -2
  23. package/dist/esm/components/obsm-list/ObsmList.js +29 -6
  24. package/dist/esm/components/offcanvas/index.js +9 -4
  25. package/dist/esm/components/scatterplot/Scatterplot.js +1 -1
  26. package/dist/esm/components/search-bar/SearchBar.js +10 -1
  27. package/dist/esm/components/search-bar/SearchResults.js +2 -2
  28. package/dist/esm/components/var-list/VarItem.js +26 -25
  29. package/dist/esm/components/var-list/VarList.js +3 -5
  30. package/dist/esm/components/violin/Violin.js +15 -5
  31. package/dist/esm/constants/constants.js +4 -1
  32. package/dist/esm/context/DatasetContext.js +1 -1
  33. package/dist/esm/context/SettingsContext.js +19 -2
  34. package/dist/esm/utils/VirtualizedList.js +2 -2
  35. package/package.json +4 -3
  36. package/scss/cherita.scss +23 -0
  37. package/scss/components/accordions.scss +11 -1
  38. package/scss/components/layouts.scss +68 -32
  39. package/scss/components/lists.scss +8 -1
@@ -165,19 +165,20 @@ function CategoricalItem(_ref2) {
165
165
  key: value,
166
166
  className: "obs-item"
167
167
  }, /*#__PURE__*/React.createElement("div", {
168
- className: "d-flex align-items-center"
168
+ className: "d-flex align-items-center flex-wrap"
169
169
  }, /*#__PURE__*/React.createElement("div", {
170
- className: "flex-grow-1"
170
+ className: "flex-grow-1 me-auto mw-100"
171
171
  }, /*#__PURE__*/React.createElement(Form.Check, {
172
172
  className: "obs-value-list-check",
173
173
  type: "switch",
174
+ title: label,
174
175
  label: label,
175
176
  checked: !isOmitted,
176
177
  onChange: () => onChange(value)
177
178
  })), /*#__PURE__*/React.createElement("div", {
178
- className: "d-flex align-items-center"
179
- }, /*#__PURE__*/React.createElement("div", {
180
- className: "pl-1 m-0 flex-grow-1"
179
+ className: "d-flex align-items-center ms-auto"
180
+ }, (!!histogramData.data || histogramData.isPending) && /*#__PURE__*/React.createElement("div", {
181
+ className: "pl-1 m-0"
181
182
  }, /*#__PURE__*/React.createElement(Histogram, {
182
183
  data: histogramData.data,
183
184
  isPending: histogramData.isPending,
@@ -192,7 +193,7 @@ function CategoricalItem(_ref2) {
192
193
  className: "d-flex align-items-center"
193
194
  }, /*#__PURE__*/React.createElement(Badge, {
194
195
  className: "value-count-badge"
195
- }, " ", isSliced && /*#__PURE__*/React.createElement(React.Fragment, null, formatNumerical(parseInt(filteredStats.value_counts)), " ", "out of", " "), formatNumerical(parseInt(stats.value_counts), FORMATS.EXPONENTIAL)), /*#__PURE__*/React.createElement("div", {
196
+ }, " ", isSliced && parseInt(filteredStats.value_counts) !== parseInt(stats.value_counts) && /*#__PURE__*/React.createElement(React.Fragment, null, formatNumerical(parseInt(filteredStats.value_counts)), " ", "out of", " "), formatNumerical(parseInt(stats.value_counts), FORMATS.EXPONENTIAL)), /*#__PURE__*/React.createElement("div", {
196
197
  className: "value-pct-gauge-container"
197
198
  }, isSliced ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Gauge, {
198
199
  className: "pct-gauge filtered-pct-gauge",
@@ -272,6 +273,7 @@ export function CategoricalObs(_ref3) {
272
273
  enabled: showHistograms
273
274
  });
274
275
  const filteredObsData = useFilteredObsData(obs);
276
+ const enabledHistograms = useMemo(() => showHistograms && settings.colorEncoding === COLOR_ENCODINGS.VAR, [settings.colorEncoding, showHistograms]);
275
277
  const getDataAtIndex = useCallback(index => {
276
278
  var _obsHistograms$fetche;
277
279
  return {
@@ -283,7 +285,7 @@ export function CategoricalObs(_ref3) {
283
285
  },
284
286
  isOmitted: _.includes(obs.omit, obs.codes[obs.values[index]]),
285
287
  label: obs.values[index],
286
- histogramData: showHistograms && settings.colorEncoding === COLOR_ENCODINGS.VAR ? {
288
+ histogramData: enabledHistograms ? {
287
289
  data: (_obsHistograms$fetche = obsHistograms.fetchedData) === null || _obsHistograms$fetche === void 0 ? void 0 : _obsHistograms$fetche[obs.values[index]],
288
290
  isPending: obsHistograms.isPending,
289
291
  altColor: isSliced
@@ -298,7 +300,7 @@ export function CategoricalObs(_ref3) {
298
300
  isSliced: isSliced,
299
301
  colors: obs.colors
300
302
  };
301
- }, [obs.values, obs.codes, obs.value_counts, obs.omit, totalCounts, showHistograms, obs.colors, settings.colorEncoding, obsHistograms.fetchedData, obsHistograms.isPending, isSliced, filteredObsData === null || filteredObsData === void 0 ? void 0 : filteredObsData.value_counts, filteredObsData === null || filteredObsData === void 0 ? void 0 : filteredObsData.pct]);
303
+ }, [obs.values, obs.codes, obs.value_counts, obs.omit, obs.colors, totalCounts, enabledHistograms, obsHistograms.fetchedData, obsHistograms.isPending, isSliced, filteredObsData === null || filteredObsData === void 0 ? void 0 : filteredObsData.value_counts, filteredObsData === null || filteredObsData === void 0 ? void 0 : filteredObsData.pct]);
302
304
  showColor &= settings.colorEncoding === COLOR_ENCODINGS.OBS;
303
305
  return /*#__PURE__*/React.createElement(ListGroup, {
304
306
  variant: "flush",
@@ -317,7 +319,11 @@ export function CategoricalObs(_ref3) {
317
319
  max: max,
318
320
  onChange: toggleObs,
319
321
  showColor: showColor,
320
- estimateSize: 42
322
+ estimateSize: i =>
323
+ // rough attempt to determine size based on label length
324
+ // estimate size of 68 pixels if label is long (>=25 chars if enabledHistograms, >=30 if showColor, >=35 otherwise), else 42
325
+ // TODO: consider isSliced as count badge will be longer ?
326
+ obs.values[i].length >= (enabledHistograms ? 25 : showColor ? 30 : 35) ? 68 : 42
321
327
  }));
322
328
  }
323
329
  function ObsContinuousStats(_ref4) {
@@ -401,6 +407,10 @@ export function ContinuousObs(_ref5) {
401
407
  enabled: showHistograms
402
408
  });
403
409
  const filteredObsData = useFilteredObsData(obs);
410
+ const enabledHistograms = useMemo(() => showHistograms && settings.colorEncoding === COLOR_ENCODINGS.VAR, [settings.colorEncoding, showHistograms]);
411
+ const getLabel = useCallback(index => {
412
+ return isNaN(obs.values[index]) ? "NaN" : getContinuousLabel(obs.codes[obs.values[index]], obs.bins.binEdges);
413
+ }, [obs.bins.binEdges, obs.codes, obs.values]);
404
414
  const getDataAtIndex = useCallback(index => {
405
415
  var _obsHistograms$fetche2;
406
416
  return {
@@ -411,8 +421,8 @@ export function ContinuousObs(_ref5) {
411
421
  pct: obs.value_counts[obs.values[index]] / totalCounts * 100
412
422
  },
413
423
  isOmitted: _.includes(obs.omit, obs.codes[obs.values[index]]),
414
- label: isNaN(obs.values[index]) ? "NaN" : getContinuousLabel(obs.codes[obs.values[index]], obs.bins.binEdges),
415
- histogramData: showHistograms && settings.colorEncoding === COLOR_ENCODINGS.VAR ? {
424
+ label: getLabel(index),
425
+ histogramData: enabledHistograms ? {
416
426
  data: (_obsHistograms$fetche2 = obsHistograms.fetchedData) === null || _obsHistograms$fetche2 === void 0 ? void 0 : _obsHistograms$fetche2[obs.values[index]],
417
427
  isPending: obsHistograms.isPending,
418
428
  altColor: isSliced
@@ -426,7 +436,7 @@ export function ContinuousObs(_ref5) {
426
436
  },
427
437
  isSliced: isSliced
428
438
  };
429
- }, [filteredObsData === null || filteredObsData === void 0 ? void 0 : filteredObsData.pct, filteredObsData === null || filteredObsData === void 0 ? void 0 : filteredObsData.value_counts, isSliced, obs.bins.binEdges, obs.codes, obs.omit, obs.value_counts, obs.values, obsHistograms.fetchedData, obsHistograms.isPending, settings.colorEncoding, showHistograms, totalCounts]);
439
+ }, [enabledHistograms, filteredObsData === null || filteredObsData === void 0 ? void 0 : filteredObsData.pct, filteredObsData === null || filteredObsData === void 0 ? void 0 : filteredObsData.value_counts, getLabel, isSliced, obs.codes, obs.omit, obs.value_counts, obs.values, obsHistograms.fetchedData, obsHistograms.isPending, totalCounts]);
430
440
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(ListGroup, {
431
441
  variant: "flush",
432
442
  className: "cherita-list"
@@ -444,7 +454,11 @@ export function ContinuousObs(_ref5) {
444
454
  max: max,
445
455
  onChange: toggleObs,
446
456
  showColor: false,
447
- estimateSize: 42
457
+ estimateSize: i =>
458
+ // rough attempt to determine size based on label length
459
+ // estimate size of 68 pixels if label is long (>=20 chars if enabledHistograms, >=30 otherwise), else 42
460
+ // TODO: consider isSliced as count badge will be longer ?
461
+ getLabel(i).length >= (enabledHistograms ? 20 : 30) ? 68 : 42
448
462
  })), /*#__PURE__*/React.createElement(ObsContinuousStats, {
449
463
  obs: obs
450
464
  }));
@@ -41,7 +41,7 @@ const ObsAccordionToggle = _ref => {
41
41
  }, /*#__PURE__*/React.createElement(FontAwesomeIcon, {
42
42
  icon: isCurrentEventKey ? faChevronDown : faChevronRight
43
43
  })), /*#__PURE__*/React.createElement("span", {
44
- className: "obs-accordion-header-title"
44
+ className: "obs-accordion-header-span"
45
45
  }, children));
46
46
  };
47
47
  export function ObsColsList(_ref2) {
@@ -224,7 +224,11 @@ export function ObsColsList(_ref2) {
224
224
  }, /*#__PURE__*/React.createElement(ObsAccordionToggle, {
225
225
  eventKey: item.name,
226
226
  handleAccordionToggle: handleAccordionToggle
227
- }, /*#__PURE__*/React.createElement("div", null, item.name), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("span", {
227
+ }, /*#__PURE__*/React.createElement("div", {
228
+ className: "accordion-header-title"
229
+ }, item.name), /*#__PURE__*/React.createElement("div", {
230
+ className: "accordion-header-toolbar"
231
+ }, /*#__PURE__*/React.createElement("span", {
228
232
  className: "mx-1 cursor-pointer ".concat(inLabelObs ? "active-icon" : "text-muted opacity-50"),
229
233
  onClick: event => {
230
234
  event.stopPropagation();
@@ -4,7 +4,9 @@ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object
4
4
  function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
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 React, { useEffect, useState } from "react";
7
+ import _ from "lodash";
7
8
  import { Button, ButtonGroup, Dropdown, DropdownButton, OverlayTrigger, Tooltip } from "react-bootstrap";
9
+ import { DEFAULT_OBSM_KEYS } from "../../constants/constants";
8
10
  import { useDataset } from "../../context/DatasetContext";
9
11
  import { useSettings, useSettingsDispatch } from "../../context/SettingsContext";
10
12
  import { useFetch } from "../../utils/requests";
@@ -36,13 +38,34 @@ export function ObsmKeysList() {
36
38
  useEffect(() => {
37
39
  if (!isPending && !serverError && fetchedData) {
38
40
  setObsmKeysList(fetchedData);
41
+
42
+ // Set default obsm if in keys list and not selected
43
+ if (!settings.selectedObsm && !!fetchedData.length) {
44
+ // Follow DEFAULT_OBSM_KEYS order
45
+ _.each(DEFAULT_OBSM_KEYS, k => {
46
+ const defaultObsm = _.find(fetchedData, item => item.toLowerCase() === k);
47
+ if (defaultObsm) {
48
+ dispatch({
49
+ type: "select.obsm",
50
+ obsm: defaultObsm
51
+ });
52
+ return false; // break
53
+ }
54
+ });
55
+ }
56
+ if (settings.selectedObsm) {
57
+ // If selected obsm is not in keys list, reset to null
58
+ if (!_.includes(fetchedData, settings.selectedObsm)) {
59
+ dispatch({
60
+ type: "select.obsm",
61
+ obsm: null
62
+ });
63
+ } else {
64
+ setActive(settings.selectedObsm);
65
+ }
66
+ }
39
67
  }
40
- }, [fetchedData, isPending, serverError]);
41
- useEffect(() => {
42
- if (settings.selectedObsm) {
43
- setActive(settings.selectedObsm);
44
- }
45
- }, [settings.selectedObsm]);
68
+ }, [dispatch, fetchedData, isPending, serverError, settings.selectedObsm]);
46
69
  const obsmList = obsmKeysList.map(item => {
47
70
  return /*#__PURE__*/React.createElement(Dropdown.Item, {
48
71
  key: item,
@@ -45,14 +45,19 @@ export function OffcanvasVars(_ref3) {
45
45
  } = _ref3;
46
46
  return /*#__PURE__*/React.createElement(Offcanvas, {
47
47
  show: show,
48
- onHide: handleClose
48
+ onHide: handleClose,
49
+ className: "offcanvas-vars"
49
50
  }, /*#__PURE__*/React.createElement(Offcanvas.Header, {
50
51
  closeButton: true
51
- }, /*#__PURE__*/React.createElement(Offcanvas.Title, null, "Features")), /*#__PURE__*/React.createElement(Offcanvas.Body, null, /*#__PURE__*/React.createElement(SearchBar, {
52
+ }, /*#__PURE__*/React.createElement(Offcanvas.Title, null, "Features")), /*#__PURE__*/React.createElement(Offcanvas.Body, null, /*#__PURE__*/React.createElement("div", {
53
+ className: "sidebar-features"
54
+ }, /*#__PURE__*/React.createElement(SearchBar, {
52
55
  searchDiseases: true
53
- }), /*#__PURE__*/React.createElement(VarNamesList, {
56
+ }), /*#__PURE__*/React.createElement("div", {
57
+ className: "sidebar-features-list"
58
+ }, /*#__PURE__*/React.createElement(VarNamesList, {
54
59
  mode: mode
55
- })));
60
+ })))));
56
61
  }
57
62
  export function OffcanvasControls(_ref4) {
58
63
  let {
@@ -146,7 +146,7 @@ export function Scatterplot(_ref) {
146
146
  });
147
147
  });
148
148
  }
149
- }, [settings.selectedObsm, getRadiusScale, obsmData.data, obsmData.isPending, obsmData.serverError, data.positions]);
149
+ }, [getRadiusScale, obsmData.data, obsmData.isPending, obsmData.serverError, data.positions]);
150
150
  const getBounds = useCallback(() => {
151
151
  var _deckRef$current3, _deckRef$current4;
152
152
  const {
@@ -6,6 +6,7 @@ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e =
6
6
  import React, { useState } from "react";
7
7
  import CloseIcon from "@mui/icons-material/Close";
8
8
  import SearchIcon from "@mui/icons-material/Search";
9
+ import _ from "lodash";
9
10
  import { Button, Form, FormGroup, InputGroup, Modal } from "react-bootstrap";
10
11
  import Col from "react-bootstrap/Col";
11
12
  import Container from "react-bootstrap/Container";
@@ -15,7 +16,7 @@ import Tab from "react-bootstrap/Tab";
15
16
  import { DiseaseInfo, VarInfo } from "./SearchInfo";
16
17
  import { DiseasesSearchResults, VarSearchResults } from "./SearchResults";
17
18
  import { COLOR_ENCODINGS } from "../../constants/constants";
18
- function onVarSelect(dispatch, item) {
19
+ const select = (dispatch, item) => {
19
20
  dispatch({
20
21
  type: "select.var",
21
22
  var: item
@@ -28,6 +29,14 @@ function onVarSelect(dispatch, item) {
28
29
  type: "set.colorEncoding",
29
30
  value: COLOR_ENCODINGS.VAR
30
31
  });
32
+ };
33
+ const debounceSelect = _.debounce(select, 2000);
34
+ function onVarSelect(dispatch, item) {
35
+ dispatch({
36
+ type: "add.var",
37
+ var: item
38
+ });
39
+ debounceSelect(dispatch, item);
31
40
  }
32
41
  function addVarSet(dispatch, _ref) {
33
42
  let {
@@ -89,7 +89,7 @@ export function VarSearchResults(_ref) {
89
89
  count: deferredData.length,
90
90
  ItemComponent: ItemComponent,
91
91
  overscan: 500,
92
- estimateSize: 42,
92
+ estimateSize: () => 42,
93
93
  maxHeight: "70vh"
94
94
  }) : /*#__PURE__*/React.createElement(ListGroup.Item, {
95
95
  key: "empty",
@@ -160,7 +160,7 @@ export function DiseasesSearchResults(_ref2) {
160
160
  count: deferredData.length,
161
161
  ItemComponent: ItemComponent,
162
162
  overscan: 250,
163
- estimateSize: 32,
163
+ estimateSize: () => 32,
164
164
  maxHeight: "70vh"
165
165
  }) : /*#__PURE__*/React.createElement(ListGroup.Item, {
166
166
  key: "empty",
@@ -113,14 +113,17 @@ export function SelectionItem(_ref3) {
113
113
  });
114
114
  const hasDiseaseInfo = !isPending && !serverError && !!(fetchedData !== null && fetchedData !== void 0 && fetchedData.length);
115
115
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
116
- className: "d-flex justify-content-between ".concat(hasDiseaseInfo ? "cursor-pointer" : ""),
116
+ className: hasDiseaseInfo ? "cursor-pointer" : "",
117
117
  onClick: () => {
118
118
  setOpenInfo(o => !o);
119
119
  }
120
120
  }, /*#__PURE__*/React.createElement("div", {
121
- className: "d-flex justify-content-between align-items-center w-100"
122
- }, /*#__PURE__*/React.createElement("div", null, item.name), /*#__PURE__*/React.createElement("div", {
123
- className: "d-flex align-items-center gap-1"
121
+ className: "d-flex align-items-center"
122
+ }, /*#__PURE__*/React.createElement("div", {
123
+ className: "var-item-name",
124
+ title: item.name
125
+ }, item.name), /*#__PURE__*/React.createElement("div", {
126
+ className: "ms-auto d-flex align-items-center gap-1"
124
127
  }, hasDiseaseInfo && /*#__PURE__*/React.createElement(MoreVert, null), /*#__PURE__*/React.createElement(VarHistogram, {
125
128
  item: item
126
129
  }), showSetColorEncoding && /*#__PURE__*/React.createElement(Button, {
@@ -159,6 +162,24 @@ export function SelectionItem(_ref3) {
159
162
  data: fetchedData
160
163
  }))));
161
164
  }
165
+ const select = (dispatch, mode, item) => {
166
+ if (mode === SELECTION_MODES.SINGLE) {
167
+ dispatch({
168
+ type: "select.var",
169
+ var: item
170
+ });
171
+ } else if (mode === SELECTION_MODES.MULTIPLE) {
172
+ dispatch({
173
+ type: "select.multivar",
174
+ var: item
175
+ });
176
+ }
177
+ dispatch({
178
+ type: "set.colorEncoding",
179
+ value: COLOR_ENCODINGS.VAR
180
+ });
181
+ };
182
+ const debounceSelect = _.debounce(select, 200);
162
183
  export function VarItem(_ref4) {
163
184
  let {
164
185
  item,
@@ -167,27 +188,7 @@ export function VarItem(_ref4) {
167
188
  } = _ref4;
168
189
  const settings = useSettings();
169
190
  const dispatch = useSettingsDispatch();
170
- const selectVar = () => {
171
- if (mode === SELECTION_MODES.SINGLE) {
172
- dispatch({
173
- type: "select.var",
174
- var: item
175
- });
176
- dispatch({
177
- type: "set.colorEncoding",
178
- value: COLOR_ENCODINGS.VAR
179
- });
180
- } else if (mode === SELECTION_MODES.MULTIPLE) {
181
- dispatch({
182
- type: "select.multivar",
183
- var: item
184
- });
185
- dispatch({
186
- type: "set.colorEncoding",
187
- value: COLOR_ENCODINGS.VAR
188
- });
189
- }
190
- };
191
+ const selectVar = () => debounceSelect(dispatch, mode, item);
191
192
  const removeVar = () => {
192
193
  dispatch({
193
194
  type: "remove.var",
@@ -131,9 +131,7 @@ export function VarNamesList(_ref) {
131
131
  };
132
132
  const isPending = varMeans.isPending && settings.varSort.var.sort === VAR_SORT.MATRIX;
133
133
  return /*#__PURE__*/React.createElement("div", {
134
- className: "position-relative"
135
- }, /*#__PURE__*/React.createElement("div", {
136
- className: "overflow-auto mt-3"
134
+ className: "mt-3 d-flex flex-column h-100"
137
135
  }, /*#__PURE__*/React.createElement("div", {
138
136
  className: "d-flex justify-content-between mb-2"
139
137
  }, /*#__PURE__*/React.createElement("h5", null, _.capitalize(displayName)), /*#__PURE__*/React.createElement(ButtonGroup, {
@@ -164,9 +162,9 @@ export function VarNamesList(_ref) {
164
162
  }), "Clear"))), /*#__PURE__*/React.createElement(React.Fragment, null, !varList.length ? /*#__PURE__*/React.createElement(Alert, {
165
163
  variant: "light"
166
164
  }, "Search for a feature.") : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(VarListToolbar, null), /*#__PURE__*/React.createElement("div", {
167
- className: "position-relative"
165
+ className: "overflow-auto flex-grow-1 modern-scrollbars"
168
166
  }, isPending && /*#__PURE__*/React.createElement(LoadingSpinner, null), /*#__PURE__*/React.createElement(ListGroup, {
169
167
  variant: "flush",
170
168
  className: "cherita-list"
171
- }, varList))))));
169
+ }, varList)))));
172
170
  }
@@ -135,8 +135,15 @@ export function Violin(_ref) {
135
135
  if (!serverError) {
136
136
  if (hasSelections) {
137
137
  return /*#__PURE__*/React.createElement("div", {
138
- className: "cherita-plot cherita-violin position-relative"
139
- }, isPending && /*#__PURE__*/React.createElement(LoadingSpinner, null), /*#__PURE__*/React.createElement(Plot, {
138
+ className: "cherita-plot cherita-violin"
139
+ }, isPending && /*#__PURE__*/React.createElement(LoadingSpinner, null), /*#__PURE__*/React.createElement("div", {
140
+ className: "d-flex flex-column h-100"
141
+ }, /*#__PURE__*/React.createElement("div", {
142
+ className: "flex-grow-1 position-relative",
143
+ style: {
144
+ minHeight: "0"
145
+ }
146
+ }, /*#__PURE__*/React.createElement(Plot, {
140
147
  data: data,
141
148
  layout: layout,
142
149
  useResizeHandler: true,
@@ -148,14 +155,17 @@ export function Violin(_ref) {
148
155
  displaylogo: false,
149
156
  modeBarButtons: modeBarButtons
150
157
  }
151
- }), (fetchedData === null || fetchedData === void 0 ? void 0 : fetchedData.resampled) && /*#__PURE__*/React.createElement(Alert, {
152
- variant: "warning"
158
+ })), (fetchedData === null || fetchedData === void 0 ? void 0 : fetchedData.resampled) && /*#__PURE__*/React.createElement("div", {
159
+ className: "flex-shrink-0"
160
+ }, /*#__PURE__*/React.createElement(Alert, {
161
+ variant: "warning",
162
+ className: "mb-1"
153
163
  }, /*#__PURE__*/React.createElement("b", null, "Warning:"), " For performance reasons this plot was generated with resampled data. It will not be exactly the same as one produced with the entire dataset. \xA0", /*#__PURE__*/React.createElement(OverlayTrigger, {
154
164
  placement: "top",
155
165
  overlay: /*#__PURE__*/React.createElement(Tooltip, null, "Resampled to 100K values following a Monte Carlo style approach to help ensure resampled data is a good representation of the original dataset's distribution.")
156
166
  }, /*#__PURE__*/React.createElement(FontAwesomeIcon, {
157
167
  icon: faCircleInfo
158
- }))));
168
+ }))))));
159
169
  }
160
170
  return /*#__PURE__*/React.createElement("div", {
161
171
  className: "cherita-violin"
@@ -97,4 +97,7 @@ export const PLOTLY_MODEBAR_BUTTONS = ["toImage", "zoom2d", "pan2d", "zoomIn2d",
97
97
  export const BREAKPOINTS = {
98
98
  LG: "(max-width: 991.98px)",
99
99
  XL: "(max-width: 1199.98px)"
100
- };
100
+ };
101
+
102
+ // In order of priority
103
+ export const DEFAULT_OBSM_KEYS = ["x_umap", "x_tsne", "x_scvi", "x_pca"];
@@ -50,7 +50,7 @@ const persistOptions = {
50
50
  return false;
51
51
  }
52
52
  },
53
- buster: "1.4.0" || "0.0.0"
53
+ buster: "1.4.1-dev.2025-06-30.e26168b5" || "0.0.0"
54
54
  // @TODO: add maxAge and api version numbers as buster
55
55
  };
56
56
  const initialDataset = {
@@ -1,8 +1,11 @@
1
+ const _excluded = ["buster", "timestamp"];
1
2
  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
3
  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
4
  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
5
  function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
5
6
  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); }
7
+ function _objectWithoutProperties(e, t) { if (null == e) return {}; var o, r, i = _objectWithoutPropertiesLoose(e, t); if (Object.getOwnPropertySymbols) { var n = Object.getOwnPropertySymbols(e); for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]); } return i; }
8
+ function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
6
9
  import React, { createContext, useContext, useEffect, useReducer } from "react";
7
10
  import _ from "lodash";
8
11
  import { COLOR_ENCODINGS, DOTPLOT_SCALES, LOCAL_STORAGE_KEY, MATRIXPLOT_SCALES, OBS_TYPES, PSEUDOSPATIAL_CATEGORICAL_MODES, VAR_SORT, VAR_SORT_ORDER, VIOLINPLOT_SCALES } from "../constants/constants";
@@ -117,7 +120,18 @@ export function SettingsProvider(_ref2) {
117
120
  const DATASET_STORAGE_KEY = "".concat(LOCAL_STORAGE_KEY, "-").concat(dataset_url);
118
121
  // Use localStorage directly instead of useLocalStorage due to unnecessary re-renders
119
122
  // https://github.com/uidotdev/usehooks/issues/157
120
- const localSettings = JSON.parse(localStorage.getItem(DATASET_STORAGE_KEY)) || {};
123
+ let _ref3 = JSON.parse(localStorage.getItem(DATASET_STORAGE_KEY)) || {},
124
+ {
125
+ buster,
126
+ timestamp
127
+ } = _ref3,
128
+ localSettings = _objectWithoutProperties(_ref3, _excluded);
129
+
130
+ // If the buster is not set or does not match the current package version,
131
+ // reset localSettings to avoid stale data
132
+ if (!buster || buster !== "1.4.1-dev.2025-06-30.e26168b5") {
133
+ localSettings = {};
134
+ }
121
135
  const [settings, dispatch] = useReducer(settingsReducer, {
122
136
  canOverrideSettings,
123
137
  defaultSettings,
@@ -126,7 +140,10 @@ export function SettingsProvider(_ref2) {
126
140
  useEffect(() => {
127
141
  if (canOverrideSettings) {
128
142
  try {
129
- localStorage.setItem(DATASET_STORAGE_KEY, JSON.stringify(settings));
143
+ localStorage.setItem(DATASET_STORAGE_KEY, JSON.stringify(_objectSpread({
144
+ buster: "1.4.1-dev.2025-06-30.e26168b5" || "0.0.0",
145
+ timestamp: Date.now()
146
+ }, settings)));
130
147
  } catch (err) {
131
148
  if (err.code === 22 || err.code === 1014 || err.name === "QuotaExceededError" || err.name === "NS_ERROR_DOM_QUOTA_REACHED") {
132
149
  console.err("Browser storage quota exceeded");
@@ -10,7 +10,7 @@ export function VirtualizedList(_ref) {
10
10
  getDataAtIndex,
11
11
  count,
12
12
  ItemComponent,
13
- estimateSize = 45,
13
+ estimateSize = () => 45,
14
14
  overscan = 25,
15
15
  maxHeight = "65vh"
16
16
  } = _ref,
@@ -19,7 +19,7 @@ export function VirtualizedList(_ref) {
19
19
  const itemVirtualizer = useVirtualizer({
20
20
  count: count,
21
21
  getScrollElement: () => parentNode,
22
- estimateSize: () => estimateSize,
22
+ estimateSize: i => estimateSize(i),
23
23
  overscan: overscan
24
24
  });
25
25
  const refCallback = useCallback(node => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haniffalab/cherita-react",
3
- "version": "1.4.0",
3
+ "version": "1.4.1-dev.2025-06-30.e26168b5",
4
4
  "author": "Haniffa Lab",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -127,5 +127,6 @@
127
127
  "bugs": {
128
128
  "url": "https://github.com/haniffalab/cherita-react/issues"
129
129
  },
130
- "homepage": "https://github.com/haniffalab/cherita-react#readme"
131
- }
130
+ "homepage": "https://github.com/haniffalab/cherita-react#readme",
131
+ "prereleaseSha": "e26168b526ee80a8dfb494c61e68a64112c8dbc1"
132
+ }
package/scss/cherita.scss CHANGED
@@ -21,6 +21,10 @@ $prefix: "bs-" !default;
21
21
  align-items: center;
22
22
  }
23
23
 
24
+ .cursor-pointer {
25
+ cursor: pointer;
26
+ }
27
+
24
28
  .cherita-container {
25
29
  margin: 40px auto;
26
30
  background-color: #fff;
@@ -151,6 +155,13 @@ $prefix: "bs-" !default;
151
155
  padding-right: 1rem;
152
156
  word-break: auto-phrase;
153
157
  overflow-wrap: anywhere;
158
+
159
+ .form-check-label {
160
+ white-space: nowrap;
161
+ overflow: hidden;
162
+ text-overflow: ellipsis;
163
+ display: block;
164
+ }
154
165
  }
155
166
 
156
167
  .grad-step {
@@ -404,4 +415,16 @@ $gauge-padding: 0.15rem;
404
415
  white-space: nowrap;
405
416
  overflow: hidden;
406
417
  text-overflow: ellipsis;
418
+ }
419
+
420
+ .var-item-name {
421
+ @extend .me-auto;
422
+ padding-right: 0.5rem;
423
+ white-space: nowrap;
424
+ overflow: hidden;
425
+ text-overflow: ellipsis;
426
+ }
427
+
428
+ input[type="checkbox"] {
429
+ cursor: pointer;
407
430
  }
@@ -19,12 +19,22 @@
19
19
  height: 20px;
20
20
  margin-right: 10px;
21
21
  }
22
- .obs-accordion-header-title {
22
+ .obs-accordion-header-span {
23
23
  font-size: 1rem;
24
24
  width: 100%;
25
25
  display: flex;
26
26
  justify-content: space-between;
27
27
  align-items: center;
28
+
29
+ .accordion-header-title {
30
+ @extend .text-break;
31
+ @extend .me-1;
32
+ }
33
+
34
+ .accordion-header-toolbar {
35
+ @extend .d-flex;
36
+ @extend .flex-nowrap;
37
+ }
28
38
  }
29
39
  .active-icon {
30
40
  color: #0c63e4;