@vitessce/biomarker-select 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.js +32224 -0
  3. package/dist-tsc/BiomarkerSelectSubscriber.d.ts +14 -0
  4. package/dist-tsc/BiomarkerSelectSubscriber.d.ts.map +1 -0
  5. package/dist-tsc/BiomarkerSelectSubscriber.js +53 -0
  6. package/dist-tsc/cart.d.ts +2 -0
  7. package/dist-tsc/cart.d.ts.map +1 -0
  8. package/dist-tsc/cart.js +27 -0
  9. package/dist-tsc/confirmatory-stepper.d.ts +2 -0
  10. package/dist-tsc/confirmatory-stepper.d.ts.map +1 -0
  11. package/dist-tsc/confirmatory-stepper.js +65 -0
  12. package/dist-tsc/default-async-functions.d.ts +26 -0
  13. package/dist-tsc/default-async-functions.d.ts.map +1 -0
  14. package/dist-tsc/default-async-functions.js +168 -0
  15. package/dist-tsc/index.d.ts +3 -0
  16. package/dist-tsc/index.d.ts.map +1 -0
  17. package/dist-tsc/index.js +2 -0
  18. package/dist-tsc/missing-types.d.ts +3 -0
  19. package/dist-tsc/missing-types.d.ts.map +1 -0
  20. package/dist-tsc/missing-types.js +1 -0
  21. package/dist-tsc/scmd-ui.d.ts +2 -0
  22. package/dist-tsc/scmd-ui.d.ts.map +1 -0
  23. package/dist-tsc/scmd-ui.js +15 -0
  24. package/dist-tsc/select-agnostic.d.ts +2 -0
  25. package/dist-tsc/select-agnostic.d.ts.map +1 -0
  26. package/dist-tsc/select-agnostic.js +66 -0
  27. package/dist-tsc/select-specific.d.ts +2 -0
  28. package/dist-tsc/select-specific.d.ts.map +1 -0
  29. package/dist-tsc/select-specific.js +61 -0
  30. package/dist-tsc/select-stratification.d.ts +2 -0
  31. package/dist-tsc/select-stratification.d.ts.map +1 -0
  32. package/dist-tsc/select-stratification.js +18 -0
  33. package/dist-tsc/styles.d.ts +2 -0
  34. package/dist-tsc/styles.d.ts.map +1 -0
  35. package/dist-tsc/styles.js +32 -0
  36. package/package.json +52 -0
  37. package/src/BiomarkerSelectSubscriber.js +107 -0
  38. package/src/cart.js +95 -0
  39. package/src/confirmatory-stepper.js +209 -0
  40. package/src/default-async-functions.js +187 -0
  41. package/src/index.js +5 -0
  42. package/src/missing-types.ts +2 -0
  43. package/src/scmd-ui.js +75 -0
  44. package/src/select-agnostic.js +172 -0
  45. package/src/select-specific.js +111 -0
  46. package/src/select-stratification.js +90 -0
  47. package/src/styles.js +33 -0
@@ -0,0 +1,32 @@
1
+ import { makeStyles } from '@material-ui/core';
2
+ export const useStyles = makeStyles(() => ({
3
+ header: {
4
+ marginTop: '10px',
5
+ marginBottom: '10px',
6
+ },
7
+ fullWidthBox: {
8
+ width: '100%',
9
+ },
10
+ selectButton: {
11
+ position: 'absolute',
12
+ right: 0,
13
+ },
14
+ accordion: {
15
+ width: '100%',
16
+ margin: '0 !important',
17
+ },
18
+ accordionDetails: {
19
+ paddingTop: 0,
20
+ },
21
+ cartUl: {
22
+ marginTop: 0,
23
+ marginBottom: 0,
24
+ },
25
+ selectInput: {
26
+ height: 'auto !important',
27
+ },
28
+ searchInput: {
29
+ lineHeight: 'initial',
30
+ height: 'auto !important',
31
+ },
32
+ }));
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@vitessce/biomarker-select",
3
+ "version": "3.5.0",
4
+ "author": "Gehlenborg Lab",
5
+ "homepage": "http://vitessce.io",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/vitessce/vitessce.git"
9
+ },
10
+ "license": "MIT",
11
+ "type": "module",
12
+ "main": "dist/index.js",
13
+ "files": [
14
+ "src",
15
+ "dist",
16
+ "dist-tsc"
17
+ ],
18
+ "dependencies": {
19
+ "@material-ui/core": "~4.12.3",
20
+ "@material-ui/icons": "~4.11.2",
21
+ "@material-ui/lab": "^4.0.0-alpha.61",
22
+ "@mui/x-data-grid": "^4.0.2",
23
+ "@tanstack/react-query": "^4.29.12",
24
+ "react-window": "^1.8.10",
25
+ "fuse.js": "^7.0.0",
26
+ "d3-dsv": "^1.1.1",
27
+ "@vitessce/constants-internal": "3.5.0",
28
+ "@vitessce/vit-s": "3.5.0",
29
+ "@vitessce/types": "3.5.0"
30
+ },
31
+ "devDependencies": {
32
+ "@testing-library/jest-dom": "^5.16.4",
33
+ "@testing-library/react": "^13.3.0",
34
+ "react": "^18.0.0",
35
+ "vite": "^4.3.0",
36
+ "vitest": "^0.32.2"
37
+ },
38
+ "peerDependencies": {
39
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
40
+ },
41
+ "scripts": {
42
+ "bundle": "pnpm exec vite build -c ../../../scripts/vite.config.js",
43
+ "test": "pnpm exec vitest --run"
44
+ },
45
+ "module": "dist/index.js",
46
+ "exports": {
47
+ ".": {
48
+ "types": "./dist-tsc/index.d.ts",
49
+ "import": "./dist/index.js"
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,107 @@
1
+ /* eslint-disable max-len */
2
+ /* eslint-disable no-unused-vars */
3
+ import React, { useState, useCallback } from 'react';
4
+ import {
5
+ useAsyncFunction,
6
+ useViewConfigStoreApi,
7
+ useSetViewConfig,
8
+ useViewConfig,
9
+ useCoordination,
10
+ } from '@vitessce/vit-s';
11
+ import { AsyncFunctionType, ViewType, COMPONENT_COORDINATION_TYPES } from '@vitessce/constants-internal';
12
+ import { ScmdUi } from './scmd-ui.js';
13
+
14
+
15
+ /**
16
+ *
17
+ * @param {object} props
18
+ * @param {{ name: string, stratificationType: string, sampleSets: [string[], string[]]}[]} props.stratificationOptions
19
+ * @returns
20
+ */
21
+ export function BiomarkerSelectSubscriber(props) {
22
+ const {
23
+ coordinationScopes,
24
+ stratificationOptions,
25
+ } = props;
26
+
27
+ const [{
28
+ sampleSetSelection,
29
+ featureSelection,
30
+ }, {
31
+ setSampleSetSelection,
32
+ setFeatureSelection,
33
+ }] = useCoordination(
34
+ COMPONENT_COORDINATION_TYPES[ViewType.BIOMARKER_SELECT],
35
+ coordinationScopes,
36
+ );
37
+
38
+ const viewConfigStoreApi = useViewConfigStoreApi();
39
+ const viewConfig = useViewConfig();
40
+ const setViewConfig = useSetViewConfig(viewConfigStoreApi);
41
+
42
+
43
+ // TODO: make isSelecting a coordination type plugin.
44
+ // TODO: use store hooks from @vitessce/vit-s to update the view config based on the selections.
45
+ const [isSelecting, setIsSelecting] = useState(true);
46
+
47
+ const [mode, setMode] = useState(null);
48
+ const [step, setStep] = useState(null);
49
+ const [currentModalityAgnosticSelection, setCurrentModalityAgnosticSelection] = useState(null);
50
+ const [currentModalitySpecificSelection, setCurrentModalitySpecificSelection] = useState(null);
51
+ const [currentStratificationSelection, setCurrentStratificationSelection] = useState(null);
52
+
53
+ const autocompleteFeature = useAsyncFunction(AsyncFunctionType.AUTOCOMPLETE_FEATURE);
54
+ const transformFeature = useAsyncFunction(AsyncFunctionType.TRANSFORM_FEATURE);
55
+
56
+ const autocompleteNode = useCallback(
57
+ async inputValue => autocompleteFeature(inputValue),
58
+ [autocompleteFeature],
59
+ );
60
+ const getEdges = useCallback(
61
+ async (node, targetModality) => transformFeature(node, targetModality),
62
+ [transformFeature],
63
+ );
64
+
65
+ return (
66
+ <>
67
+ {isSelecting ? (
68
+ <ScmdUi
69
+ mode={mode}
70
+ setMode={setMode}
71
+ step={step}
72
+ setStep={setStep}
73
+ currentModalityAgnosticSelection={currentModalityAgnosticSelection}
74
+ setCurrentModalityAgnosticSelection={setCurrentModalityAgnosticSelection}
75
+ currentModalitySpecificSelection={currentModalitySpecificSelection}
76
+ setCurrentModalitySpecificSelection={setCurrentModalitySpecificSelection}
77
+ currentStratificationSelection={currentStratificationSelection}
78
+ setCurrentStratificationSelection={setCurrentStratificationSelection}
79
+ autocompleteNode={autocompleteNode}
80
+ getEdges={getEdges}
81
+ stratifications={stratificationOptions}
82
+ onFinish={() => {
83
+ // eslint-disable-next-line no-console
84
+ console.log('Done selecting');
85
+ /*
86
+ console.log(viewConfig);
87
+ setViewConfig({
88
+ ...viewConfig,
89
+ coordinationSpace: {
90
+ ...viewConfig.coordinationSpace,
91
+ sampleSetFilter: {
92
+ ...viewConfig.coordinationSpace.sampleSetFilter,
93
+ 'case-control': currentStratificationSelection?.sampleSets,
94
+ },
95
+ sampleSetSelection: {
96
+ ...viewConfig.coordinationSpace.sampleSetSelection,
97
+ 'case-control': currentStratificationSelection?.sampleSets,
98
+ },
99
+ },
100
+ });
101
+ */
102
+ }}
103
+ />
104
+ ) : null}
105
+ </>
106
+ );
107
+ }
package/src/cart.js ADDED
@@ -0,0 +1,95 @@
1
+ import React from 'react';
2
+ import { Accordion, AccordionDetails, AccordionSummary, Typography } from '@material-ui/core';
3
+ import { ExpandMore as ExpandMoreIcon } from '@material-ui/icons';
4
+ import { useStyles } from './styles.js';
5
+
6
+
7
+ export function ConfirmatoryCart(props) {
8
+ const {
9
+ currentModalityAgnosticSelection,
10
+ // eslint-disable-next-line no-unused-vars
11
+ setCurrentModalityAgnosticSelection,
12
+ currentModalitySpecificSelection,
13
+ // eslint-disable-next-line no-unused-vars
14
+ setCurrentModalitySpecificSelection,
15
+ currentStratificationSelection,
16
+ // eslint-disable-next-line no-unused-vars
17
+ setCurrentStratificationSelection,
18
+ } = props;
19
+ const classes = useStyles();
20
+ const [expandedAccordions, setExpandedAccordions] = React.useState([
21
+ 'agnostic', 'specific', 'stratification',
22
+ ]);
23
+
24
+ const handleAccordionChange = panel => (event, isExpanded) => {
25
+ if (isExpanded) {
26
+ setExpandedAccordions([...expandedAccordions, panel]);
27
+ } else {
28
+ setExpandedAccordions(expandedAccordions.filter(item => item !== panel));
29
+ }
30
+ };
31
+
32
+ return (
33
+ <>
34
+ <Typography>Current selections</Typography>
35
+ <Accordion className={classes.accordion} expanded={expandedAccordions.includes('agnostic')} onChange={handleAccordionChange('agnostic')}>
36
+ <AccordionSummary
37
+ expandIcon={<ExpandMoreIcon />}
38
+ aria-controls="panel1bh-content"
39
+ id="panel1bh-header"
40
+ >
41
+ <Typography>
42
+ Modality-agnostic biomarkers
43
+ </Typography>
44
+ </AccordionSummary>
45
+ <AccordionDetails className={classes.accordionDetails}>
46
+ <ul className={classes.cartUl}>
47
+ {currentModalityAgnosticSelection ? (
48
+ currentModalityAgnosticSelection.map(item => (
49
+ <li key={item.kgId}>{item.label} ({item.nodeType})</li>
50
+ ))
51
+ ) : null}
52
+ </ul>
53
+ </AccordionDetails>
54
+ </Accordion>
55
+ <Accordion className={classes.accordion} expanded={expandedAccordions.includes('specific')} onChange={handleAccordionChange('specific')}>
56
+ <AccordionSummary
57
+ expandIcon={<ExpandMoreIcon />}
58
+ aria-controls="panel2bh-content"
59
+ id="panel2bh-header"
60
+ >
61
+ <Typography>
62
+ Modality-specific signature
63
+ </Typography>
64
+ </AccordionSummary>
65
+ <AccordionDetails className={classes.accordionDetails}>
66
+ <ul className={classes.cartUl}>
67
+ {currentModalitySpecificSelection ? (
68
+ currentModalitySpecificSelection.map(item => (
69
+ <li key={item.kgId}>{item.label} ({item.nodeType})</li>
70
+ ))
71
+ ) : null}
72
+ </ul>
73
+ </AccordionDetails>
74
+ </Accordion>
75
+ <Accordion className={classes.accordion} expanded={expandedAccordions.includes('stratification')} onChange={handleAccordionChange('stratification')}>
76
+ <AccordionSummary
77
+ expandIcon={<ExpandMoreIcon />}
78
+ aria-controls="panel3bh-content"
79
+ id="panel3bh-header"
80
+ >
81
+ <Typography>
82
+ Case-control stratification
83
+ </Typography>
84
+ </AccordionSummary>
85
+ <AccordionDetails className={classes.accordionDetails}>
86
+ <ul className={classes.cartUl}>
87
+ {currentStratificationSelection ? (
88
+ <li>{currentStratificationSelection.name}</li>
89
+ ) : null}
90
+ </ul>
91
+ </AccordionDetails>
92
+ </Accordion>
93
+ </>
94
+ );
95
+ }
@@ -0,0 +1,209 @@
1
+ import React, { useEffect } from 'react';
2
+ import { Stepper, Step, StepLabel, Button, Grid, Box, Typography } from '@material-ui/core';
3
+ import { ConfirmatoryCart } from './cart.js';
4
+ import { SelectAgnostic } from './select-agnostic.js';
5
+ import { SelectSpecific } from './select-specific.js';
6
+ import { SelectStratification } from './select-stratification.js';
7
+ import { useStyles } from './styles.js';
8
+
9
+ const steps = [
10
+ 'Select biomarkers (modality-agnostic)',
11
+ 'Derive modality-specific biomarker signature',
12
+ 'Define case-control stratification',
13
+ ];
14
+
15
+ export function ConfirmatoryStepper(props) {
16
+ const {
17
+ autocompleteNode,
18
+
19
+ currentModalityAgnosticSelection,
20
+ setCurrentModalityAgnosticSelection,
21
+ currentModalitySpecificSelection,
22
+ setCurrentModalitySpecificSelection,
23
+ currentStratificationSelection,
24
+ setCurrentStratificationSelection,
25
+
26
+ getEdges,
27
+ stratifications,
28
+ onFinish,
29
+ } = props;
30
+ const classes = useStyles();
31
+ const [activeStep, setActiveStep] = React.useState(0);
32
+ const [skipped, setSkipped] = React.useState(new Set());
33
+
34
+ const isStepOptional = step => step === 2;
35
+
36
+ const isStepSkipped = step => skipped.has(step);
37
+
38
+ const handleNext = () => {
39
+ let newSkipped = skipped;
40
+ if (isStepSkipped(activeStep)) {
41
+ newSkipped = new Set(newSkipped.values());
42
+ newSkipped.delete(activeStep);
43
+ }
44
+
45
+ setActiveStep(prevActiveStep => prevActiveStep + 1);
46
+ setSkipped(newSkipped);
47
+ };
48
+
49
+ const handleBack = () => {
50
+ setActiveStep(prevActiveStep => prevActiveStep - 1);
51
+ };
52
+
53
+ const handleSkip = () => {
54
+ if (!isStepOptional(activeStep)) {
55
+ // You probably want to guard against something like this,
56
+ // it should never occur unless someone's actively trying to break something.
57
+ throw new Error("You can't skip a step that isn't optional.");
58
+ }
59
+
60
+ setActiveStep(prevActiveStep => prevActiveStep + 1);
61
+ setSkipped((prevSkipped) => {
62
+ const newSkipped = new Set(prevSkipped.values());
63
+ newSkipped.add(activeStep);
64
+ return newSkipped;
65
+ });
66
+ };
67
+
68
+ const handleReset = () => {
69
+ setActiveStep(0);
70
+ };
71
+
72
+ useEffect(() => {
73
+ if (activeStep === steps.length) {
74
+ onFinish();
75
+ }
76
+ }, [activeStep, steps]);
77
+
78
+
79
+ return (
80
+ <Grid item container xs={12}>
81
+ <Grid item container xs={12}>
82
+ <Box className={classes.fullWidthBox}>
83
+ <Stepper activeStep={activeStep}>
84
+ {steps.map((label, index) => {
85
+ const stepProps = {};
86
+ const labelProps = {};
87
+ if (isStepOptional(index)) {
88
+ labelProps.optional = (
89
+ <Typography variant="caption">Optional</Typography>
90
+ );
91
+ }
92
+ if (isStepSkipped(index)) {
93
+ stepProps.completed = false;
94
+ }
95
+ return (
96
+ <Step key={label} {...stepProps}>
97
+ <StepLabel {...labelProps}>{label}</StepLabel>
98
+ </Step>
99
+ );
100
+ })}
101
+ </Stepper>
102
+ </Box>
103
+ </Grid>
104
+ <Grid item container xs={12} style={{ marginTop: '20px' }} alignItems="flex-start">
105
+ {/* Step contents to left of cart */}
106
+ <Grid item container xs={8}>
107
+ {activeStep === steps.length ? (
108
+ <>
109
+ <Typography style={{ marginTop: '20px', marginBottom: '10px' }}>
110
+ All steps completed - you&apos;re finished
111
+ </Typography>
112
+ </>
113
+ ) : (
114
+ <>
115
+ {activeStep === 0 ? (
116
+ <SelectAgnostic
117
+ autocompleteNode={autocompleteNode}
118
+
119
+ currentModalityAgnosticSelection={currentModalityAgnosticSelection}
120
+ setCurrentModalityAgnosticSelection={setCurrentModalityAgnosticSelection}
121
+ currentModalitySpecificSelection={currentModalitySpecificSelection}
122
+ setCurrentModalitySpecificSelection={setCurrentModalitySpecificSelection}
123
+ currentStratificationSelection={currentStratificationSelection}
124
+ setCurrentStratificationSelection={setCurrentStratificationSelection}
125
+
126
+ getEdges={getEdges}
127
+ stratifications={stratifications}
128
+ />
129
+ ) : null}
130
+ {activeStep === 1 ? (
131
+ <SelectSpecific
132
+ autocompleteNode={autocompleteNode}
133
+
134
+ currentModalityAgnosticSelection={currentModalityAgnosticSelection}
135
+ setCurrentModalityAgnosticSelection={setCurrentModalityAgnosticSelection}
136
+ currentModalitySpecificSelection={currentModalitySpecificSelection}
137
+ setCurrentModalitySpecificSelection={setCurrentModalitySpecificSelection}
138
+ currentStratificationSelection={currentStratificationSelection}
139
+ setCurrentStratificationSelection={setCurrentStratificationSelection}
140
+
141
+ getEdges={getEdges}
142
+ stratifications={stratifications}
143
+ />
144
+ ) : null}
145
+ {activeStep === 2 ? (
146
+ <SelectStratification
147
+ autocompleteNode={autocompleteNode}
148
+
149
+ currentModalityAgnosticSelection={currentModalityAgnosticSelection}
150
+ setCurrentModalityAgnosticSelection={setCurrentModalityAgnosticSelection}
151
+ currentModalitySpecificSelection={currentModalitySpecificSelection}
152
+ setCurrentModalitySpecificSelection={setCurrentModalitySpecificSelection}
153
+ currentStratificationSelection={currentStratificationSelection}
154
+ setCurrentStratificationSelection={setCurrentStratificationSelection}
155
+
156
+ getEdges={getEdges}
157
+ stratifications={stratifications}
158
+ />
159
+ ) : null}
160
+ </>
161
+ )}
162
+ </Grid>
163
+ {/* Cart */}
164
+ <Grid item container xs={4}>
165
+ <ConfirmatoryCart
166
+ currentModalityAgnosticSelection={currentModalityAgnosticSelection}
167
+ setCurrentModalityAgnosticSelection={setCurrentModalityAgnosticSelection}
168
+ currentModalitySpecificSelection={currentModalitySpecificSelection}
169
+ setCurrentModalitySpecificSelection={setCurrentModalitySpecificSelection}
170
+ currentStratificationSelection={currentStratificationSelection}
171
+ setCurrentStratificationSelection={setCurrentStratificationSelection}
172
+ stratifications={stratifications}
173
+ />
174
+ </Grid>
175
+ {/* Step navigation button footer */}
176
+ <Box className={classes.fullWidthBox}>
177
+ {activeStep === steps.length ? (
178
+ <>
179
+ <Box style={{ display: 'flex', flexDirection: 'row', paddingTop: '20px' }}>
180
+ <Box style={{ flex: '1 1 auto' }} />
181
+ <Button onClick={handleReset}>Reset</Button>
182
+ </Box>
183
+ </>
184
+ ) : (
185
+ <Box style={{ display: 'flex', flexDirection: 'row', paddingTop: '20px' }}>
186
+ <Button
187
+ color="inherit"
188
+ disabled={activeStep === 0}
189
+ onClick={handleBack}
190
+ style={{ mr: 1 }}
191
+ >
192
+ Back
193
+ </Button>
194
+ <Box style={{ flex: '1 1 auto' }} />
195
+ {isStepOptional(activeStep) && (
196
+ <Button color="inherit" onClick={handleSkip} style={{ marginRight: '10px' }}>
197
+ Skip
198
+ </Button>
199
+ )}
200
+ <Button onClick={handleNext}>
201
+ {activeStep === steps.length - 1 ? 'Finish' : 'Next'}
202
+ </Button>
203
+ </Box>
204
+ )}
205
+ </Box>
206
+ </Grid>
207
+ </Grid>
208
+ );
209
+ }
@@ -0,0 +1,187 @@
1
+ // @ts-check
2
+ /* eslint-disable max-len */
3
+ /* eslint-disable no-unused-vars */
4
+ import { csvParse } from 'd3-dsv';
5
+ // eslint-disable-next-line import/no-unresolved
6
+ import Fuse from 'fuse.js/basic';
7
+
8
+ /** @import { KgNode, KgEdge, TargetModalityType, AutocompleteFeatureFunc, TransformFeatureFunc } from '@vitessce/types' */
9
+ /** @import { QueryClient, QueryFunctionContext } from '@tanstack/react-query' */
10
+
11
+ const KG_BASE_URL = 'https://storage.googleapis.com/vitessce-demo-data/enrichr-kg-september-2023';
12
+
13
+ /**
14
+ * @returns {Promise<KgNode[]>}
15
+ */
16
+ function loadGeneNodes() {
17
+ return fetch(`${KG_BASE_URL}/Gene.nodes.csv`)
18
+ .then(res => res.text())
19
+ .then((res) => {
20
+ const result = csvParse(res);
21
+ return result.map((/** @type {any} */ d) => ({
22
+ kgId: d.id,
23
+ label: d.label,
24
+ term: `hgnc.symbol:${d.label}`,
25
+ nodeType: 'gene',
26
+ }));
27
+ });
28
+ }
29
+
30
+ /**
31
+ * @returns {Promise<KgNode[]>}
32
+ */
33
+ function loadCellTypeNodes() {
34
+ return fetch(`${KG_BASE_URL}/HuBMAP_ASCTplusB_augmented_2022.nodes.csv`)
35
+ .then(res => res.text())
36
+ .then((res) => {
37
+ const result = csvParse(res);
38
+ return result.map((/** @type {any} */ d) => ({
39
+ kgId: d.id,
40
+ label: d.label,
41
+ term: d.cell_id.length ? d.cell_id : null,
42
+ nodeType: 'cell-type',
43
+ }));
44
+ });
45
+ }
46
+
47
+ /**
48
+ * @returns {Promise<KgNode[]>}
49
+ */
50
+ function loadPathwayNodes() {
51
+ return fetch(`${KG_BASE_URL}/Reactome_2022.nodes.csv`)
52
+ .then(res => res.text())
53
+ .then((res) => {
54
+ const result = csvParse(res);
55
+ return result.map((/** @type {any} */ d) => ({
56
+ kgId: d.id,
57
+ label: d.pathway,
58
+ term: `reactome:${d.acc}`,
59
+ nodeType: 'pathway',
60
+ }));
61
+ });
62
+ }
63
+
64
+ // Parent app can pass in queryClient
65
+ // (to every async function) so that functions can call .fetchQuery
66
+ // Reference: https://tanstack.com/query/latest/docs/reference/QueryClient#queryclientfetchquery
67
+
68
+ /**
69
+ *
70
+ * @param {QueryFunctionContext} ctx
71
+ * @returns
72
+ */
73
+ async function loadFuseObject(ctx) {
74
+ const queryClient = /** @type {QueryClient} */ (ctx.meta?.queryClient);
75
+
76
+ const [geneNodes, cellTypeNodes, pathwayNodes] = await Promise.all([
77
+ queryClient.fetchQuery({
78
+ queryKey: ['geneNodes'],
79
+ staleTime: Infinity,
80
+ queryFn: loadGeneNodes,
81
+ }),
82
+ queryClient.fetchQuery({
83
+ queryKey: ['cellTypeNodes'],
84
+ staleTime: Infinity,
85
+ queryFn: loadCellTypeNodes,
86
+ }),
87
+ queryClient.fetchQuery({
88
+ queryKey: ['pathwayNodes'],
89
+ staleTime: Infinity,
90
+ queryFn: loadPathwayNodes,
91
+ }),
92
+ ]);
93
+
94
+ const fuseOptions = {
95
+ keys: ['label', 'term'],
96
+ };
97
+
98
+ // For fuzzy searching using fuse.js
99
+ return new Fuse(
100
+ [
101
+ ...geneNodes,
102
+ ...cellTypeNodes,
103
+ ...pathwayNodes,
104
+ ],
105
+ fuseOptions,
106
+ );
107
+ }
108
+
109
+ /**
110
+ * @satisfies {AutocompleteFeatureFunc}
111
+ * @param {object} ctx
112
+ * @param {QueryClient} ctx.queryClient
113
+ * @param {string} partial
114
+ * @param {null | TargetModalityType} targetModality
115
+ * @returns {Promise<KgNode[]>}
116
+ */
117
+ export async function autocompleteFeature({ queryClient }, partial, targetModality) {
118
+ if (partial.length < 1) {
119
+ return [];
120
+ }
121
+ // TODO: parameterize the fuse object (via queryKey) based on targetModality?
122
+ // Or just perform that filtering after the search?
123
+ const fuse = await queryClient.fetchQuery({
124
+ queryKey: ['autocompleteFeature', 'fuseObject'],
125
+ staleTime: Infinity,
126
+ queryFn: loadFuseObject,
127
+ meta: { queryClient },
128
+ });
129
+
130
+ const results = fuse.search(partial);
131
+ return results.map((/** @type {any} */ result) => result.item);
132
+ }
133
+
134
+ async function loadPathwayToGeneEdges() {
135
+ return fetch(`${KG_BASE_URL}/Reactome_2022.Reactome.Gene.edges.csv`)
136
+ .then(res => res.text())
137
+ .then((res) => {
138
+ const result = csvParse(res);
139
+ return result.map((/** @type {any} */ d) => ({
140
+ source: d.source,
141
+ target: d.target,
142
+ relation: d.relation,
143
+ }));
144
+ });
145
+ }
146
+
147
+ /**
148
+ * @satisfies {TransformFeatureFunc}
149
+ * @param {object} ctx
150
+ * @param {QueryClient} ctx.queryClient
151
+ * @param {KgNode} node
152
+ * @param {TargetModalityType} targetModality
153
+ * @returns {Promise<KgNode[]>}
154
+ */
155
+ export async function transformFeature({ queryClient }, node, targetModality) {
156
+ if (targetModality === node.nodeType) {
157
+ // For example, if the target modality is gene and the node is already a gene node.
158
+ return [node];
159
+ }
160
+ if (targetModality === 'gene') {
161
+ const geneNodes = await queryClient.fetchQuery({
162
+ queryKey: ['geneNodes'],
163
+ staleTime: Infinity,
164
+ queryFn: loadGeneNodes,
165
+ });
166
+ if (node.nodeType === 'pathway') {
167
+ // Pathway to genes case.
168
+ const pathwayGeneEdges = await queryClient.fetchQuery({
169
+ queryKey: ['pathwayGeneEdges'],
170
+ staleTime: Infinity,
171
+ queryFn: loadPathwayToGeneEdges,
172
+ });
173
+
174
+ const matchingEdges = pathwayGeneEdges.filter((/** @type {KgEdge} */ d) => d.source === node.kgId);
175
+ const matchingGeneIds = matchingEdges.map((/** @type {KgEdge} */ d) => d.target);
176
+ const matchingGenes = geneNodes.filter(d => matchingGeneIds.includes(d.kgId));
177
+ return matchingGenes;
178
+ }
179
+ if (node.nodeType === 'cell-type') {
180
+ // Cell type to genes case.
181
+
182
+ // TODO: implement
183
+ }
184
+ }
185
+ // TODO: handle other target modalities
186
+ return [];
187
+ }
package/src/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { BiomarkerSelectSubscriber } from './BiomarkerSelectSubscriber.js';
2
+ export {
3
+ autocompleteFeature,
4
+ transformFeature,
5
+ } from './default-async-functions.js';
@@ -0,0 +1,2 @@
1
+ declare module 'd3-dsv';
2
+ declare module 'fuse.js/basic'