@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.
- package/LICENSE +21 -0
- package/dist/index.js +32224 -0
- package/dist-tsc/BiomarkerSelectSubscriber.d.ts +14 -0
- package/dist-tsc/BiomarkerSelectSubscriber.d.ts.map +1 -0
- package/dist-tsc/BiomarkerSelectSubscriber.js +53 -0
- package/dist-tsc/cart.d.ts +2 -0
- package/dist-tsc/cart.d.ts.map +1 -0
- package/dist-tsc/cart.js +27 -0
- package/dist-tsc/confirmatory-stepper.d.ts +2 -0
- package/dist-tsc/confirmatory-stepper.d.ts.map +1 -0
- package/dist-tsc/confirmatory-stepper.js +65 -0
- package/dist-tsc/default-async-functions.d.ts +26 -0
- package/dist-tsc/default-async-functions.d.ts.map +1 -0
- package/dist-tsc/default-async-functions.js +168 -0
- package/dist-tsc/index.d.ts +3 -0
- package/dist-tsc/index.d.ts.map +1 -0
- package/dist-tsc/index.js +2 -0
- package/dist-tsc/missing-types.d.ts +3 -0
- package/dist-tsc/missing-types.d.ts.map +1 -0
- package/dist-tsc/missing-types.js +1 -0
- package/dist-tsc/scmd-ui.d.ts +2 -0
- package/dist-tsc/scmd-ui.d.ts.map +1 -0
- package/dist-tsc/scmd-ui.js +15 -0
- package/dist-tsc/select-agnostic.d.ts +2 -0
- package/dist-tsc/select-agnostic.d.ts.map +1 -0
- package/dist-tsc/select-agnostic.js +66 -0
- package/dist-tsc/select-specific.d.ts +2 -0
- package/dist-tsc/select-specific.d.ts.map +1 -0
- package/dist-tsc/select-specific.js +61 -0
- package/dist-tsc/select-stratification.d.ts +2 -0
- package/dist-tsc/select-stratification.d.ts.map +1 -0
- package/dist-tsc/select-stratification.js +18 -0
- package/dist-tsc/styles.d.ts +2 -0
- package/dist-tsc/styles.d.ts.map +1 -0
- package/dist-tsc/styles.js +32 -0
- package/package.json +52 -0
- package/src/BiomarkerSelectSubscriber.js +107 -0
- package/src/cart.js +95 -0
- package/src/confirmatory-stepper.js +209 -0
- package/src/default-async-functions.js +187 -0
- package/src/index.js +5 -0
- package/src/missing-types.ts +2 -0
- package/src/scmd-ui.js +75 -0
- package/src/select-agnostic.js +172 -0
- package/src/select-specific.js +111 -0
- package/src/select-stratification.js +90 -0
- 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'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