@haniffalab/cherita-react 1.0.0-dev.2025-03-13.bda9e1a6 → 1.0.0-dev.2025-03-24.e68f9e22

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 (95) 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 +11 -9
  14. package/dist/{components → cjs/components}/var-list/VarItem.js +32 -27
  15. package/dist/{components → cjs/components}/var-list/VarList.js +21 -12
  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/esm/components/dotplot/Dotplot.js +135 -0
  34. package/dist/esm/components/dotplot/DotplotControls.js +148 -0
  35. package/dist/esm/components/full-page/FullPage.js +134 -0
  36. package/dist/esm/components/full-page/FullPagePseudospatial.js +151 -0
  37. package/dist/esm/components/heatmap/Heatmap.js +105 -0
  38. package/dist/esm/components/heatmap/HeatmapControls.js +23 -0
  39. package/dist/esm/components/matrixplot/Matrixplot.js +107 -0
  40. package/dist/esm/components/matrixplot/MatrixplotControls.js +38 -0
  41. package/dist/esm/components/obs-list/ObsItem.js +477 -0
  42. package/dist/esm/components/obs-list/ObsList.js +256 -0
  43. package/dist/esm/components/obs-list/ObsToolbar.js +58 -0
  44. package/dist/esm/components/obsm-list/ObsmList.js +72 -0
  45. package/dist/esm/components/offcanvas/index.js +67 -0
  46. package/dist/esm/components/pseudospatial/Pseudospatial.js +228 -0
  47. package/dist/esm/components/pseudospatial/PseudospatialToolbar.js +123 -0
  48. package/dist/esm/components/scatterplot/Scatterplot.js +394 -0
  49. package/dist/esm/components/scatterplot/ScatterplotControls.js +71 -0
  50. package/dist/esm/components/scatterplot/SpatialControls.js +140 -0
  51. package/dist/esm/components/scatterplot/Toolbox.js +25 -0
  52. package/dist/esm/components/search-bar/SearchBar.js +74 -0
  53. package/dist/esm/components/search-bar/SearchResults.js +139 -0
  54. package/dist/esm/components/var-list/VarItem.js +250 -0
  55. package/dist/esm/components/var-list/VarList.js +272 -0
  56. package/dist/esm/components/var-list/VarListToolbar.js +84 -0
  57. package/dist/esm/components/var-list/VarSet.js +193 -0
  58. package/dist/esm/components/violin/Violin.js +141 -0
  59. package/dist/esm/components/violin/ViolinControls.js +24 -0
  60. package/dist/esm/constants/colorscales.js +22 -0
  61. package/dist/esm/constants/constants.js +84 -0
  62. package/dist/esm/context/DatasetContext.js +572 -0
  63. package/dist/esm/context/FilterContext.js +48 -0
  64. package/dist/esm/context/ZarrDataContext.js +26 -0
  65. package/dist/esm/helpers/color-helper.js +66 -0
  66. package/dist/esm/helpers/map-helper.js +53 -0
  67. package/dist/esm/helpers/zarr-helper.js +129 -0
  68. package/dist/esm/index.js +22 -0
  69. package/dist/esm/utils/Filter.js +147 -0
  70. package/dist/esm/utils/Histogram.js +44 -0
  71. package/dist/esm/utils/ImageViewer.js +27 -0
  72. package/dist/esm/utils/Legend.js +58 -0
  73. package/dist/esm/utils/LoadingIndicators.js +22 -0
  74. package/dist/esm/utils/VirtualizedList.js +55 -0
  75. package/dist/esm/utils/errors.js +47 -0
  76. package/dist/esm/utils/requests.js +116 -0
  77. package/dist/esm/utils/search.js +39 -0
  78. package/dist/esm/utils/string.js +59 -0
  79. package/dist/esm/utils/zarrData.js +102 -0
  80. package/package.json +16 -5
  81. /package/dist/{components → cjs/components}/dotplot/Dotplot.js +0 -0
  82. /package/dist/{components → cjs/components}/dotplot/DotplotControls.js +0 -0
  83. /package/dist/{components → cjs/components}/heatmap/Heatmap.js +0 -0
  84. /package/dist/{components → cjs/components}/heatmap/HeatmapControls.js +0 -0
  85. /package/dist/{components → cjs/components}/matrixplot/Matrixplot.js +0 -0
  86. /package/dist/{components → cjs/components}/matrixplot/MatrixplotControls.js +0 -0
  87. /package/dist/{components → cjs/components}/obsm-list/ObsmList.js +0 -0
  88. /package/dist/{components → cjs/components}/scatterplot/ScatterplotControls.js +0 -0
  89. /package/dist/{components → cjs/components}/violin/ViolinControls.js +0 -0
  90. /package/dist/{constants → cjs/constants}/colorscales.js +0 -0
  91. /package/dist/{constants → cjs/constants}/constants.js +0 -0
  92. /package/dist/{index.js → cjs/index.js} +0 -0
  93. /package/dist/{utils → cjs/utils}/Filter.js +0 -0
  94. /package/dist/{utils → cjs/utils}/errors.js +0 -0
  95. /package/dist/{utils → cjs/utils}/search.js +0 -0
@@ -0,0 +1,256 @@
1
+ import React, { useContext, useEffect, useState } from "react";
2
+ import { faDroplet, faListOl, faScissors, faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
3
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
4
+ import _ from "lodash";
5
+ import { Alert, Badge } from "react-bootstrap";
6
+ import Accordion from "react-bootstrap/Accordion";
7
+ import { useAccordionButton } from "react-bootstrap/AccordionButton";
8
+ import AccordionContext from "react-bootstrap/AccordionContext";
9
+ import { CategoricalObs, ContinuousObs } from "./ObsItem";
10
+ import { COLOR_ENCODINGS, OBS_TYPES } from "../../constants/constants";
11
+ import { useDataset, useDatasetDispatch } from "../../context/DatasetContext";
12
+ import { LoadingSpinner } from "../../utils/LoadingIndicators";
13
+ import { useFetch } from "../../utils/requests";
14
+ export function ObsColsList(_ref) {
15
+ let {
16
+ showColor = true
17
+ } = _ref;
18
+ const ENDPOINT = "obs/cols";
19
+ const dataset = useDataset();
20
+ const dispatch = useDatasetDispatch();
21
+ const [obsCols, setObsCols] = useState(null);
22
+ const [active, setActive] = useState(dataset.selectedObs?.name);
23
+ const [expandedItems, setExpandedItems] = useState(active ? {
24
+ [active]: true
25
+ } : {});
26
+ const [params, setParams] = useState({
27
+ url: dataset.url
28
+ });
29
+ useEffect(() => {
30
+ setParams(p => {
31
+ return {
32
+ ...p,
33
+ url: dataset.url
34
+ };
35
+ });
36
+ }, [dataset.url]);
37
+ const {
38
+ fetchedData,
39
+ isPending,
40
+ serverError
41
+ } = useFetch(ENDPOINT, params, {
42
+ refetchOnMount: false
43
+ });
44
+ useEffect(() => {
45
+ if (!isPending && !serverError) {
46
+ let filteredData = fetchedData;
47
+ if (dataset.obsCols) {
48
+ filteredData = _.filter(filteredData, d => {
49
+ return _.includes(dataset.obsCols, d.name);
50
+ });
51
+ }
52
+ setObsCols(_.keyBy(_.map(filteredData, d => {
53
+ return {
54
+ ...d,
55
+ codesMap: _.invert(d.codes),
56
+ omit: []
57
+ };
58
+ }), "name"));
59
+ }
60
+ }, [dataset.obsCols, fetchedData, isPending, serverError]);
61
+
62
+ // @TODO: fix re-rendering performance issue
63
+ useEffect(() => {
64
+ if (obsCols) {
65
+ if (obsCols[dataset.selectedObs?.name]) {
66
+ setActive(dataset.selectedObs?.name);
67
+ } else {
68
+ setActive(null);
69
+ }
70
+ }
71
+ }, [dataset.selectedObs, obsCols]);
72
+ const updateObs = updatedObs => {
73
+ setObsCols(o => {
74
+ return {
75
+ ...o,
76
+ [updatedObs.name]: updatedObs
77
+ };
78
+ });
79
+ };
80
+ const handleAccordionToggle = itemName => {
81
+ _.delay(
82
+ // to avoid contents of accordion disappearing while closing
83
+ () => {
84
+ setExpandedItems(prev => {
85
+ return {
86
+ ...prev,
87
+ [itemName]: !prev[itemName]
88
+ };
89
+ });
90
+ }, expandedItems[itemName] ? 250 : 0);
91
+ };
92
+ const toggleAll = item => {
93
+ const omit = item.omit.length ? [] : _.map(item.values, v => item.codes[v]);
94
+ setObsCols(o => {
95
+ return {
96
+ ...o,
97
+ [item.name]: {
98
+ ...item,
99
+ omit: omit
100
+ }
101
+ };
102
+ });
103
+ if (active === item.name) {
104
+ dispatch({
105
+ type: "select.obs",
106
+ obs: {
107
+ ...item,
108
+ omit: omit
109
+ }
110
+ });
111
+ }
112
+ };
113
+ const toggleLabel = item => {
114
+ const inLabelObs = _.some(dataset.labelObs, i => i.name === item.name);
115
+ if (inLabelObs) {
116
+ dispatch({
117
+ type: "remove.label.obs",
118
+ obsName: item.name
119
+ });
120
+ } else {
121
+ dispatch({
122
+ type: "add.label.obs",
123
+ obs: {
124
+ name: item.name,
125
+ type: item.type,
126
+ codesMap: item.codesMap
127
+ }
128
+ });
129
+ }
130
+ };
131
+ const toggleSlice = item => {
132
+ dispatch({
133
+ type: "toggle.slice.obs",
134
+ obs: item
135
+ });
136
+ };
137
+ const toggleColor = item => {
138
+ dispatch({
139
+ type: "select.obs",
140
+ obs: item
141
+ });
142
+ dispatch({
143
+ type: "set.colorEncoding",
144
+ value: "obs"
145
+ });
146
+ };
147
+ const toggleObs = (item, value) => {
148
+ let omit;
149
+ if (_.includes(item.omit, item.codes[value])) {
150
+ omit = item.omit.filter(i => i !== item.codes[value]);
151
+ } else {
152
+ omit = [...item.omit, item.codes[value]];
153
+ }
154
+ setObsCols(o => {
155
+ return {
156
+ ...o,
157
+ [item.name]: {
158
+ ...item,
159
+ omit: omit
160
+ }
161
+ };
162
+ });
163
+ if (active === item.name) {
164
+ dispatch({
165
+ type: "select.obs",
166
+ obs: {
167
+ ...item,
168
+ omit: omit
169
+ }
170
+ });
171
+ }
172
+ };
173
+ function ObsAccordionToggle(_ref2) {
174
+ let {
175
+ children,
176
+ eventKey
177
+ } = _ref2;
178
+ const {
179
+ activeEventKey
180
+ } = useContext(AccordionContext);
181
+ const decoratedOnClick = useAccordionButton(eventKey, () => {
182
+ handleAccordionToggle(eventKey);
183
+ });
184
+ const isCurrentEventKey = Array.isArray(activeEventKey) ? activeEventKey.includes(eventKey) : activeEventKey === eventKey;
185
+ return /*#__PURE__*/React.createElement("div", {
186
+ className: `obs-accordion-header ${isCurrentEventKey ? "active" : ""}`,
187
+ onClick: decoratedOnClick
188
+ }, /*#__PURE__*/React.createElement("span", {
189
+ className: "obs-accordion-header-chevron"
190
+ }, /*#__PURE__*/React.createElement(FontAwesomeIcon, {
191
+ icon: isCurrentEventKey ? faChevronDown : faChevronRight
192
+ })), /*#__PURE__*/React.createElement("span", {
193
+ className: "obs-accordion-header-title"
194
+ }, children));
195
+ }
196
+ const obsList = _.map(obsCols, (item, index) => {
197
+ if (item.type === OBS_TYPES.DISCRETE) {
198
+ return null;
199
+ }
200
+ const inLabelObs = _.some(dataset.labelObs, i => i.name === item.name);
201
+ const inSliceObs = dataset.sliceBy.obs && dataset.selectedObs?.name === item.name;
202
+ const isColorEncoding = dataset.colorEncoding === COLOR_ENCODINGS.OBS && dataset.selectedObs?.name === item.name;
203
+ return /*#__PURE__*/React.createElement("div", {
204
+ key: item.name
205
+ }, /*#__PURE__*/React.createElement(ObsAccordionToggle, {
206
+ eventKey: item.name
207
+ }, /*#__PURE__*/React.createElement("div", null, item.name), /*#__PURE__*/React.createElement("div", null, inLabelObs && /*#__PURE__*/React.createElement(FontAwesomeIcon, {
208
+ className: "mx-1",
209
+ icon: faListOl,
210
+ title: "In tooltip"
211
+ }), inSliceObs && /*#__PURE__*/React.createElement(FontAwesomeIcon, {
212
+ className: "mx-1",
213
+ icon: faScissors,
214
+ title: "Filter applied"
215
+ }), isColorEncoding && /*#__PURE__*/React.createElement(FontAwesomeIcon, {
216
+ className: "mx-1",
217
+ icon: faDroplet,
218
+ title: "Is color encoding"
219
+ }))), /*#__PURE__*/React.createElement(Accordion.Collapse, {
220
+ eventKey: item.name
221
+ }, /*#__PURE__*/React.createElement("div", {
222
+ className: "obs-accordion-body"
223
+ }, expandedItems[item.name] && (item.type === OBS_TYPES.CATEGORICAL || item.type === OBS_TYPES.BOOLEAN ? /*#__PURE__*/React.createElement(CategoricalObs, {
224
+ key: item.name,
225
+ obs: item,
226
+ updateObs: updateObs,
227
+ toggleAll: () => toggleAll(item),
228
+ toggleObs: value => toggleObs(item, value),
229
+ toggleLabel: () => toggleLabel(item),
230
+ toggleSlice: () => toggleSlice(item),
231
+ toggleColor: () => toggleColor(item),
232
+ showColor: showColor
233
+ }) : /*#__PURE__*/React.createElement(ContinuousObs, {
234
+ key: item.name,
235
+ obs: item,
236
+ updateObs: updateObs,
237
+ toggleAll: () => toggleAll(item),
238
+ toggleObs: value => toggleObs(item, value),
239
+ toggleLabel: () => toggleLabel(item),
240
+ toggleSlice: () => toggleSlice(item),
241
+ toggleColor: () => toggleColor(item)
242
+ })))));
243
+ });
244
+ if (!serverError) {
245
+ return /*#__PURE__*/React.createElement("div", null, isPending && /*#__PURE__*/React.createElement(LoadingSpinner, null), /*#__PURE__*/React.createElement(Accordion, {
246
+ flush: true,
247
+ defaultActiveKey: [active],
248
+ alwaysOpen: true,
249
+ className: "obs-accordion"
250
+ }, obsList));
251
+ } else {
252
+ return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(Alert, {
253
+ variant: "danger"
254
+ }, serverError.message));
255
+ }
256
+ }
@@ -0,0 +1,58 @@
1
+ import React from "react";
2
+ import { faDroplet, faListOl, faScissors } from "@fortawesome/free-solid-svg-icons";
3
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
4
+ import _ from "lodash";
5
+ import { Button, ButtonGroup, Form } from "react-bootstrap";
6
+ import { COLOR_ENCODINGS } from "../../constants/constants";
7
+ import { useDataset } from "../../context/DatasetContext";
8
+ export function ObsToolbar(_ref) {
9
+ let {
10
+ item,
11
+ showToggleAllObs = true,
12
+ showLabel = true,
13
+ showSlice = true,
14
+ showColor = true,
15
+ onToggleAllObs,
16
+ onToggleLabel,
17
+ onToggleSlice,
18
+ onToggleColor
19
+ } = _ref;
20
+ const dataset = useDataset();
21
+ const allToggledOn = !item.omit.length;
22
+ const inLabelObs = _.some(dataset.labelObs, i => i.name === item.name);
23
+ const inSliceObs = dataset.sliceBy.obs && dataset.selectedObs?.name === item.name;
24
+ const isColorEncoding = dataset.colorEncoding === COLOR_ENCODINGS.OBS && dataset.selectedObs?.name === item.name;
25
+ return /*#__PURE__*/React.createElement("div", {
26
+ className: "d-flex align-items-center"
27
+ }, /*#__PURE__*/React.createElement("div", {
28
+ className: "flex-grow-1"
29
+ }, showToggleAllObs && /*#__PURE__*/React.createElement(Form.Check, {
30
+ // prettier-ignore
31
+ type: "switch",
32
+ id: "custom-switch",
33
+ label: "Toggle all",
34
+ checked: allToggledOn,
35
+ onChange: onToggleAllObs
36
+ })), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(ButtonGroup, null, showLabel && /*#__PURE__*/React.createElement(Button, {
37
+ variant: inLabelObs ? "primary" : "outline-primary",
38
+ size: "sm",
39
+ onClick: onToggleLabel,
40
+ title: "Add to tooltip"
41
+ }, /*#__PURE__*/React.createElement(FontAwesomeIcon, {
42
+ icon: faListOl
43
+ })), showSlice && /*#__PURE__*/React.createElement(Button, {
44
+ variant: inSliceObs ? "primary" : "outline-primary",
45
+ size: "sm",
46
+ onClick: onToggleSlice,
47
+ title: "Filter to selected"
48
+ }, /*#__PURE__*/React.createElement(FontAwesomeIcon, {
49
+ icon: faScissors
50
+ })), showColor && /*#__PURE__*/React.createElement(Button, {
51
+ variant: isColorEncoding ? "primary" : "outline-primary",
52
+ size: "sm",
53
+ onClick: onToggleColor,
54
+ title: "Set as color encoding"
55
+ }, /*#__PURE__*/React.createElement(FontAwesomeIcon, {
56
+ icon: faDroplet
57
+ })))));
58
+ }
@@ -0,0 +1,72 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { Dropdown, Button, DropdownButton, ButtonGroup, OverlayTrigger, Tooltip } from "react-bootstrap";
3
+ import { useDataset, useDatasetDispatch } from "../../context/DatasetContext";
4
+ import { LoadingSpinner } from "../../utils/LoadingIndicators";
5
+ import { useFetch } from "../../utils/requests";
6
+ export function ObsmKeysList() {
7
+ const ENDPOINT = "obsm/keys";
8
+ const dataset = useDataset();
9
+ const dispatch = useDatasetDispatch();
10
+ const [obsmKeysList, setObsmKeysList] = useState([]);
11
+ const [active, setActive] = useState(null);
12
+ const [params, setParams] = useState({
13
+ url: dataset.url
14
+ });
15
+ useEffect(() => {
16
+ setParams(p => {
17
+ return {
18
+ ...p,
19
+ url: dataset.url
20
+ };
21
+ });
22
+ }, [dataset.url]);
23
+ const {
24
+ fetchedData,
25
+ isPending,
26
+ serverError
27
+ } = useFetch(ENDPOINT, params, {
28
+ refetchOnMount: false
29
+ });
30
+ useEffect(() => {
31
+ if (!isPending && !serverError) {
32
+ setObsmKeysList(fetchedData);
33
+ }
34
+ }, [fetchedData, isPending, serverError]);
35
+ useEffect(() => {
36
+ if (dataset.selectedObsm) {
37
+ setActive(dataset.selectedObsm);
38
+ }
39
+ }, [dataset.selectedObsm]);
40
+ const obsmList = obsmKeysList.map(item => {
41
+ return /*#__PURE__*/React.createElement(Dropdown.Item, {
42
+ key: item,
43
+ className: `custom ${active === item && "active"}`,
44
+ onClick: () => {
45
+ dispatch({
46
+ type: "select.obsm",
47
+ obsm: item
48
+ });
49
+ }
50
+ }, item);
51
+ });
52
+ if (!serverError) {
53
+ return /*#__PURE__*/React.createElement(React.Fragment, null, isPending && /*#__PURE__*/React.createElement(LoadingSpinner, null), /*#__PURE__*/React.createElement(DropdownButton, {
54
+ as: ButtonGroup,
55
+ title: dataset.selectedObsm || "Select an embedding",
56
+ variant: dataset.selectedObsm ? "primary" : "outline-primary",
57
+ id: "bg-nested-dropdown",
58
+ size: "sm"
59
+ }, obsmList));
60
+ } else {
61
+ return /*#__PURE__*/React.createElement(OverlayTrigger, {
62
+ placement: "top",
63
+ delay: {
64
+ show: 100,
65
+ hide: 200
66
+ },
67
+ overlay: /*#__PURE__*/React.createElement(Tooltip, null, serverError.message)
68
+ }, /*#__PURE__*/React.createElement(Button, {
69
+ variant: "danger"
70
+ }, "Error"));
71
+ }
72
+ }
@@ -0,0 +1,67 @@
1
+ import React from "react";
2
+ import Offcanvas from "react-bootstrap/Offcanvas";
3
+ import { SELECTION_MODES } from "../../constants/constants";
4
+ import { ObsColsList } from "../obs-list/ObsList";
5
+ import { ObsmKeysList } from "../obsm-list/ObsmList";
6
+ import { SearchBar } from "../search-bar/SearchBar";
7
+ import { VarNamesList } from "../var-list/VarList";
8
+ export function OffcanvasObs(_ref) {
9
+ let {
10
+ show,
11
+ handleClose,
12
+ ...props
13
+ } = _ref;
14
+ return /*#__PURE__*/React.createElement(Offcanvas, {
15
+ show: show,
16
+ onHide: handleClose,
17
+ scroll: true,
18
+ backdrop: false
19
+ }, /*#__PURE__*/React.createElement(Offcanvas.Header, {
20
+ closeButton: true
21
+ }, /*#__PURE__*/React.createElement(Offcanvas.Title, null, "Categories")), /*#__PURE__*/React.createElement(Offcanvas.Body, {
22
+ className: "p-0"
23
+ }, /*#__PURE__*/React.createElement(ObsColsList, props)));
24
+ }
25
+ export function OffcanvasObsm(_ref2) {
26
+ let {
27
+ show,
28
+ handleClose
29
+ } = _ref2;
30
+ return /*#__PURE__*/React.createElement(Offcanvas, {
31
+ show: show,
32
+ onHide: handleClose
33
+ }, /*#__PURE__*/React.createElement(Offcanvas.Header, {
34
+ closeButton: true
35
+ }, /*#__PURE__*/React.createElement(Offcanvas.Title, null, "Embedding space")), /*#__PURE__*/React.createElement(Offcanvas.Body, null, /*#__PURE__*/React.createElement(ObsmKeysList, null)));
36
+ }
37
+ export function OffcanvasVars(_ref3) {
38
+ let {
39
+ show,
40
+ handleClose,
41
+ mode = SELECTION_MODES.MULTIPLE
42
+ } = _ref3;
43
+ return /*#__PURE__*/React.createElement(Offcanvas, {
44
+ show: show,
45
+ onHide: handleClose
46
+ }, /*#__PURE__*/React.createElement(Offcanvas.Header, {
47
+ closeButton: true
48
+ }, /*#__PURE__*/React.createElement(Offcanvas.Title, null, "Features")), /*#__PURE__*/React.createElement(Offcanvas.Body, null, /*#__PURE__*/React.createElement(SearchBar, {
49
+ searchDiseases: true
50
+ }), /*#__PURE__*/React.createElement(VarNamesList, {
51
+ mode: mode
52
+ })));
53
+ }
54
+ export function OffcanvasControls(_ref4) {
55
+ let {
56
+ show,
57
+ handleClose,
58
+ Controls,
59
+ ...props
60
+ } = _ref4;
61
+ return /*#__PURE__*/React.createElement(Offcanvas, {
62
+ show: show,
63
+ onHide: handleClose
64
+ }, /*#__PURE__*/React.createElement(Offcanvas.Header, {
65
+ closeButton: true
66
+ }, /*#__PURE__*/React.createElement(Offcanvas.Title, null, "Controls")), /*#__PURE__*/React.createElement(Offcanvas.Body, null, /*#__PURE__*/React.createElement(Controls, props)));
67
+ }
@@ -0,0 +1,228 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { library } from "@fortawesome/fontawesome-svg-core";
3
+ import { faSliders } from "@fortawesome/free-solid-svg-icons";
4
+ import _ from "lodash";
5
+ import { Alert } from "react-bootstrap";
6
+ import Plot from "react-plotly.js";
7
+ import { COLOR_ENCODINGS, OBS_TYPES, PSEUDOSPATIAL_PLOT_TYPES as PLOT_TYPES } from "../../constants/constants";
8
+ import { useDataset } from "../../context/DatasetContext";
9
+ import { useFilteredData } from "../../context/FilterContext";
10
+ import { rgbToHex, useColor } from "../../helpers/color-helper";
11
+ import { ImageViewer } from "../../utils/ImageViewer";
12
+ import { Legend } from "../../utils/Legend";
13
+ import { LoadingSpinner } from "../../utils/LoadingIndicators";
14
+ import { useDebouncedFetch } from "../../utils/requests";
15
+ library.add(faSliders);
16
+ function usePseudospatialData(plotType) {
17
+ const ENDPOINT = "pseudospatial";
18
+ const dataset = useDataset();
19
+ const {
20
+ obsIndices,
21
+ isSliced
22
+ } = useFilteredData();
23
+ const baseParams = useMemo(() => {
24
+ return {
25
+ url: dataset.url,
26
+ maskSet: dataset.pseudospatial.maskSet,
27
+ maskValues: dataset.pseudospatial.maskValues,
28
+ obsIndices: isSliced ? [...(obsIndices || [])] : null,
29
+ varNamesCol: dataset.varNamesCol,
30
+ showColorbar: false,
31
+ format: "json"
32
+ };
33
+ }, [dataset.url, dataset.pseudospatial.maskSet, dataset.pseudospatial.maskValues, dataset.varNamesCol, isSliced, obsIndices]);
34
+ const getPlotParams = useCallback(() => {
35
+ if (plotType === PLOT_TYPES.GENE) {
36
+ return {
37
+ varKey: dataset.selectedVar?.isSet ? {
38
+ name: dataset.selectedVar?.name,
39
+ indices: dataset.selectedVar?.vars.map(v => v.index)
40
+ } : dataset.selectedVar?.index,
41
+ ...(dataset.sliceBy.obs ? {
42
+ obsCol: dataset.selectedObs,
43
+ obsValues: !dataset.selectedObs?.omit.length ? null : _.difference(_.values(dataset.selectedObs?.codes), dataset.selectedObs?.omit).map(c => dataset.selectedObs?.codesMap[c])
44
+ } : {})
45
+ };
46
+ } else if (plotType === PLOT_TYPES.CATEGORICAL) {
47
+ return {
48
+ obsCol: dataset.selectedObs,
49
+ obsValues: !dataset.selectedObs?.omit.length ? null : _.difference(_.values(dataset.selectedObs?.codes), dataset.selectedObs?.omit).map(c => dataset.selectedObs?.codesMap[c]),
50
+ mode: dataset.pseudospatial.categoricalMode
51
+ };
52
+ } else if (plotType === "continuous") {
53
+ return {
54
+ obsCol: dataset.selectedObs,
55
+ obsValues: !dataset.selectedObs?.omit.length ? null : _.difference(_.values(dataset.selectedObs?.codes), dataset.selectedObs?.omit).map(c => dataset.selectedObs?.codesMap[c])
56
+ };
57
+ }
58
+ }, [dataset.pseudospatial.categoricalMode, dataset.selectedObs, dataset.selectedVar?.index, dataset.selectedVar?.isSet, dataset.selectedVar?.name, dataset.selectedVar?.vars, dataset.sliceBy.obs, plotType]);
59
+ const params = useMemo(() => {
60
+ return {
61
+ ...baseParams,
62
+ ...getPlotParams()
63
+ };
64
+ }, [baseParams, getPlotParams]);
65
+ return useDebouncedFetch(ENDPOINT + "/" + plotType, params, 500, {
66
+ enabled: !!plotType && !!dataset.pseudospatial.maskSet
67
+ });
68
+ }
69
+ export function Pseudospatial(_ref) {
70
+ let {
71
+ showLegend = true,
72
+ sharedScaleRange = false,
73
+ height = 200,
74
+ setShowControls,
75
+ plotType,
76
+ setPlotType
77
+ } = _ref;
78
+ const dataset = useDataset();
79
+ const [data, setData] = useState([]);
80
+ const [layout, setLayout] = useState({});
81
+ const {
82
+ getColor
83
+ } = useColor();
84
+ const colorscale = useRef(dataset.controls.colorScale);
85
+ useEffect(() => {
86
+ setPlotType(dataset.colorEncoding === COLOR_ENCODINGS.VAR ? PLOT_TYPES.GENE : dataset.selectedObs?.type === OBS_TYPES.CATEGORICAL || dataset.selectedObs?.type === OBS_TYPES.BOOLEAN ? PLOT_TYPES.CATEGORICAL : dataset.selectedObs?.type === OBS_TYPES.CONTINUOUS ? PLOT_TYPES.CONTINUOUS : PLOT_TYPES.MASKS);
87
+ }, [dataset.colorEncoding, dataset.selectedObs?.type, setPlotType]);
88
+ const updateColorscale = useCallback(colorscale => {
89
+ setLayout(l => {
90
+ return {
91
+ ...l,
92
+ coloraxis: {
93
+ ...l.coloraxis,
94
+ colorscale: colorscale
95
+ }
96
+ };
97
+ });
98
+ setData(d => {
99
+ const min = layout?.coloraxis?.cmin;
100
+ const max = layout?.coloraxis?.cmax;
101
+ return _.map(d, trace => {
102
+ const v = trace.meta.value;
103
+ if (v === null) {
104
+ return trace;
105
+ }
106
+ const color = rgbToHex(getColor({
107
+ value: (v - min) / (max - min)
108
+ }));
109
+ return {
110
+ ...trace,
111
+ fillcolor: color,
112
+ line: {
113
+ ...trace.line,
114
+ color: color
115
+ }
116
+ };
117
+ });
118
+ });
119
+ }, [getColor, layout?.coloraxis?.cmax, layout?.coloraxis?.cmin]);
120
+ const {
121
+ fetchedData,
122
+ isPending,
123
+ serverError
124
+ } = usePseudospatialData(plotType);
125
+ useEffect(() => {
126
+ if (!isPending && !serverError) {
127
+ setData(fetchedData.data);
128
+ setLayout(fetchedData.layout);
129
+ updateColorscale(colorscale.current);
130
+ }
131
+ }, [fetchedData, isPending, serverError, sharedScaleRange, updateColorscale]);
132
+ useEffect(() => {
133
+ colorscale.current = dataset.controls.colorScale;
134
+ updateColorscale(colorscale.current);
135
+ }, [dataset.controls.colorScale, updateColorscale]);
136
+ useEffect(() => {
137
+ if (sharedScaleRange) {
138
+ const {
139
+ min,
140
+ max
141
+ } = {
142
+ min: dataset.controls.range[0] * (dataset.controls.valueRange[1] - dataset.controls.valueRange[0]) + dataset.controls.valueRange[0],
143
+ max: dataset.controls.range[1] * (dataset.controls.valueRange[1] - dataset.controls.valueRange[0]) + dataset.controls.valueRange[0]
144
+ };
145
+ setData(d => {
146
+ return _.map(d, trace => {
147
+ const v = trace.meta.value;
148
+ if (v === null) {
149
+ return trace;
150
+ }
151
+ const color = rgbToHex(getColor({
152
+ value: (v - min) / (max - min)
153
+ }));
154
+ return {
155
+ ...trace,
156
+ fillcolor: color,
157
+ line: {
158
+ ...trace.line,
159
+ color: color
160
+ }
161
+ };
162
+ });
163
+ });
164
+ setLayout(l => {
165
+ return {
166
+ ...l,
167
+ coloraxis: {
168
+ ...l.coloraxis,
169
+ cmin: min,
170
+ cmax: max
171
+ }
172
+ };
173
+ });
174
+ }
175
+ }, [dataset.controls.range, dataset.controls.valueMax, dataset.controls.valueMin, dataset.controls.valueRange, getColor, sharedScaleRange]);
176
+ const hasSelections = !!plotType && !!dataset.pseudospatial.maskSet;
177
+ const faSlidersPath = faSliders.icon[4];
178
+ if (!serverError) {
179
+ return /*#__PURE__*/React.createElement("div", {
180
+ className: "cherita-pseudospatial"
181
+ }, /*#__PURE__*/React.createElement(React.Fragment, null, hasSelections && isPending && /*#__PURE__*/React.createElement(LoadingSpinner, null), hasSelections && /*#__PURE__*/React.createElement(Plot, {
182
+ data: data,
183
+ layout: {
184
+ ...layout,
185
+ autosize: true,
186
+ height: height
187
+ },
188
+ useResizeHandler: true,
189
+ className: "cherita-pseudospatial-plot",
190
+ config: {
191
+ displaylogo: false,
192
+ displayModeBar: true,
193
+ modeBarButtonsToAdd: [{
194
+ name: "Open plot controls",
195
+ icon: {
196
+ width: 512,
197
+ height: 512,
198
+ path: faSlidersPath,
199
+ ascent: 512,
200
+ descent: 0
201
+ },
202
+ click: () => setShowControls(prev => !prev)
203
+ }]
204
+ }
205
+ }), hasSelections && showLegend && /*#__PURE__*/React.createElement(Legend, {
206
+ min: layout?.coloraxis?.cmin,
207
+ max: layout?.coloraxis?.cmax,
208
+ addText: plotType === PLOT_TYPES.GENE ? " - Mean expression" : plotType === PLOT_TYPES.CATEGORICAL ? " - %" : plotType === PLOT_TYPES.CONTINUOUS ? " - Mean value" : ""
209
+ })));
210
+ } else {
211
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Alert, {
212
+ variant: "danger"
213
+ }, serverError.message));
214
+ }
215
+ }
216
+
217
+ // @TODO: explore making it a minimap
218
+ export function PseudospatialImage() {
219
+ const dataset = useDataset();
220
+ if (dataset.imageUrl) {
221
+ return /*#__PURE__*/React.createElement(ImageViewer, {
222
+ src: dataset.imageUrl,
223
+ alt: "Pseudospatial reference image"
224
+ });
225
+ } else {
226
+ return /*#__PURE__*/React.createElement(React.Fragment, null);
227
+ }
228
+ }