@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
package/src/scmd-ui.js ADDED
@@ -0,0 +1,75 @@
1
+ /* eslint-disable no-unused-vars */
2
+ import React, { useState } from 'react';
3
+ import { Button, ButtonGroup, Grid, Tooltip } from '@material-ui/core';
4
+ import { ConfirmatoryStepper } from './confirmatory-stepper.js';
5
+ import { useStyles } from './styles.js';
6
+
7
+
8
+ export function ScmdUi(props) {
9
+ const {
10
+ mode: modeProp,
11
+ setMode,
12
+ step: stepProp,
13
+ setStep,
14
+ currentModalityAgnosticSelection,
15
+ setCurrentModalityAgnosticSelection,
16
+ currentModalitySpecificSelection,
17
+ setCurrentModalitySpecificSelection,
18
+ currentStratificationSelection,
19
+ setCurrentStratificationSelection,
20
+
21
+ autocompleteNode,
22
+
23
+ getEdges,
24
+ stratifications,
25
+ onFinish,
26
+ } = props;
27
+ const classes = useStyles();
28
+
29
+ const mode = modeProp || 'confirmatory';
30
+ const step = mode === 'confirmatory' ? (stepProp || 'select-biomarkers') : (stepProp || 'define-stratification');
31
+
32
+ const [tempIsVisible, setTempIsVisible] = useState(true);
33
+
34
+ const isConfirmatoryMode = mode === 'confirmatory';
35
+ return (
36
+ <Grid container style={{ border: '0px solid red' }}>
37
+ {/* Header */}
38
+ <Grid item container xs={12} className={classes.header}>
39
+ <Grid item xs={8} />
40
+ <Grid item container xs={4} justifyContent="flex-end">
41
+ <ButtonGroup variant="outlined" size="small" color="secondary" aria-label="Toggle between confirmatory and exploratory modes">
42
+ <Tooltip arrow title="Start from biomarker(s) of interest">
43
+ <Button variant={isConfirmatoryMode ? 'contained' : 'outlined'} onClick={() => setMode('confirmatory')}>Confirmatory (Hypothesis-driven)</Button>
44
+ </Tooltip>
45
+ <Tooltip arrow title="Lacking biomarker(s) of interest">
46
+ <Button variant={!isConfirmatoryMode ? 'contained' : 'outlined'} onClick={() => setMode('exploratory')}>Exploratory (Hypothesis-generating)</Button>
47
+ </Tooltip>
48
+ </ButtonGroup>
49
+ </Grid>
50
+ </Grid>
51
+ {/* Stepper */}
52
+ {tempIsVisible ? (
53
+ <div style={{ border: '2px solid #eee', borderRadius: '5px' }}>
54
+ {mode === 'confirmatory' ? (
55
+ <ConfirmatoryStepper
56
+ autocompleteNode={autocompleteNode}
57
+
58
+ currentModalityAgnosticSelection={currentModalityAgnosticSelection}
59
+ setCurrentModalityAgnosticSelection={setCurrentModalityAgnosticSelection}
60
+ currentModalitySpecificSelection={currentModalitySpecificSelection}
61
+ setCurrentModalitySpecificSelection={setCurrentModalitySpecificSelection}
62
+ currentStratificationSelection={currentStratificationSelection}
63
+ setCurrentStratificationSelection={setCurrentStratificationSelection}
64
+
65
+ getEdges={getEdges}
66
+ stratifications={stratifications}
67
+ onFinish={onFinish}
68
+ />
69
+ ) : null}
70
+ {mode === 'exploratory' ? <span>TODO</span> : null}
71
+ </div>
72
+ ) : null}
73
+ </Grid>
74
+ );
75
+ }
@@ -0,0 +1,172 @@
1
+ import React, { useRef, useEffect } from 'react';
2
+ import { Grid, Button, TextField, Typography } from '@material-ui/core';
3
+ import { Add as AddIcon, Info as InfoIcon } from '@material-ui/icons';
4
+ import { Autocomplete } from '@material-ui/lab';
5
+ import { VariableSizeList } from 'react-window';
6
+ import { useStyles } from './styles.js';
7
+
8
+
9
+ const LIST_ROW_HEIGHT = 48;
10
+
11
+ // We use a context to pass the extra props from ListboxComponent to the outer div element.
12
+ const OuterElementContext = React.createContext({});
13
+
14
+ const OuterElementType = React.forwardRef((props, ref) => {
15
+ const outerProps = React.useContext(OuterElementContext);
16
+ return <div ref={ref} {...props} {...outerProps} />;
17
+ });
18
+
19
+
20
+ function ListRow(props) {
21
+ const { data, index, style } = props;
22
+
23
+ return React.cloneElement(data[index], {
24
+ style: {
25
+ ...style,
26
+ top: style.top + 8,
27
+ },
28
+ });
29
+ }
30
+
31
+ function useResetCache(itemCount) {
32
+ const ref = useRef(null);
33
+ useEffect(() => {
34
+ if (ref.current != null) {
35
+ ref.current.resetAfterIndex(0, true);
36
+ }
37
+ }, [itemCount]);
38
+ return ref;
39
+ }
40
+
41
+ // Reference: https://mui.com/material-ui/react-autocomplete/#virtualization
42
+ const ListboxComponent = React.forwardRef((props, ref) => {
43
+ const { children, ...other } = props;
44
+
45
+ const itemCount = children.length;
46
+ const gridRef = useResetCache(itemCount);
47
+
48
+ return (
49
+ <div ref={ref}>
50
+ <OuterElementContext.Provider value={other}>
51
+ <VariableSizeList
52
+ height={Math.min(8, itemCount) * LIST_ROW_HEIGHT}
53
+ itemSize={() => LIST_ROW_HEIGHT}
54
+ width="100%"
55
+ innerElementType="ul"
56
+ outerElementType={OuterElementType}
57
+ overscanCount={5}
58
+ ref={gridRef}
59
+ itemCount={itemCount}
60
+ itemData={children}
61
+ >
62
+ {ListRow}
63
+ </VariableSizeList>
64
+ </OuterElementContext.Provider>
65
+ </div>
66
+ );
67
+ });
68
+
69
+ export function SelectAgnostic(props) {
70
+ const {
71
+ autocompleteNode,
72
+ currentModalityAgnosticSelection,
73
+ setCurrentModalityAgnosticSelection,
74
+ } = props;
75
+ const classes = useStyles();
76
+
77
+ const [selectedItem, setSelectedItem] = React.useState(null);
78
+
79
+ const [potentialItems, setPotentialItems] = React.useState([]);
80
+
81
+ const handleChange = async (event) => {
82
+ const { value } = event.target;
83
+ if (!value) {
84
+ return;
85
+ }
86
+ const autocompleteResult = await autocompleteNode(value);
87
+ setPotentialItems(autocompleteResult);
88
+ };
89
+
90
+
91
+ function confirmSelectedItem() {
92
+ // eslint-disable-next-line max-len
93
+ if (selectedItem && !currentModalityAgnosticSelection?.find(item => item.kgId === selectedItem?.kgId)) {
94
+ setCurrentModalityAgnosticSelection([
95
+ ...(currentModalityAgnosticSelection || []),
96
+ selectedItem,
97
+ ]);
98
+ }
99
+ }
100
+ return (
101
+ <>
102
+ <Grid item container xs={12}>
103
+ <Typography variant="h6">
104
+ Search by gene, protein, pathway (by term name), or cell type:
105
+ </Typography>
106
+ </Grid>
107
+ <Grid item container xs={12}>
108
+ <Grid item xs={4}>
109
+ <Autocomplete
110
+ options={potentialItems}
111
+ autoComplete
112
+ includeInputInList
113
+ onInputChange={handleChange}
114
+ onChange={(event, item) => setSelectedItem(item)}
115
+ classes={{ input: classes.searchInput }}
116
+ renderInput={params => (
117
+ <TextField label="Search" variant="outlined" onChange={handleChange} {...params} />
118
+ )}
119
+ ListboxComponent={ListboxComponent}
120
+ getOptionLabel={option => option.label}
121
+ renderOption={option => (
122
+ <Typography noWrap>{option.label} ({option.nodeType})</Typography>
123
+ )}
124
+ />
125
+ </Grid>
126
+ <Grid
127
+ item
128
+ xs={8}
129
+ style={{
130
+ border: selectedItem ? '1px solid gray' : '1px solid transparent',
131
+ borderRadius: '4px',
132
+ }}
133
+ >
134
+ {selectedItem ? (
135
+ <>
136
+ <Grid container item xs={12}>
137
+ <Grid item xs={9}>
138
+ <Typography variant="h4" title={selectedItem.term}>
139
+ {selectedItem.label}
140
+ </Typography>
141
+ </Grid>
142
+ <Grid item xs={3} style={{ position: 'relative' }}>
143
+ <Button
144
+ className={classes.selectButton}
145
+ variant="contained"
146
+ startIcon={<AddIcon />}
147
+ onClick={confirmSelectedItem}
148
+ >
149
+ Select
150
+ </Button>
151
+ </Grid>
152
+ </Grid>
153
+ <Grid container item xs={12}>
154
+ <InfoIcon />
155
+ <Typography variant="h6">About this {selectedItem.nodeType}</Typography>
156
+ </Grid>
157
+ <Grid container item xs={12} key={selectedItem.term}>
158
+ <iframe
159
+ title={`Embedded metadata page for ontology term ${selectedItem.term}`}
160
+ src={`https://identifiers.org/${selectedItem.term}`}
161
+ width="100%"
162
+ height="500"
163
+ style={{ border: 0 }}
164
+ />
165
+ </Grid>
166
+ </>
167
+ ) : null}
168
+ </Grid>
169
+ </Grid>
170
+ </>
171
+ );
172
+ }
@@ -0,0 +1,111 @@
1
+ import React, { useMemo } from 'react';
2
+ import { useQueries } from '@tanstack/react-query';
3
+ import { Grid, FormHelperText, Typography, Select, FormControl } from '@material-ui/core';
4
+ import { DataGrid } from '@mui/x-data-grid';
5
+ import { useStyles } from './styles.js';
6
+
7
+
8
+ const columns = [
9
+ {
10
+ field: 'targetLabel',
11
+ headerName: 'Gene symbol',
12
+ width: 200,
13
+ editable: false,
14
+ },
15
+ {
16
+ field: 'sourceRelationship',
17
+ headerName: 'Relationship to modality-agnostic selections',
18
+ width: 400,
19
+ editable: false,
20
+ },
21
+ ];
22
+
23
+
24
+ export function SelectSpecific(props) {
25
+ const {
26
+ currentModalityAgnosticSelection,
27
+ currentModalitySpecificSelection,
28
+ setCurrentModalitySpecificSelection,
29
+ getEdges,
30
+ } = props;
31
+ const classes = useStyles();
32
+
33
+ const queries = useQueries({
34
+ queries: currentModalityAgnosticSelection?.map(item => ({
35
+ queryKey: [item.nodeType, item.kgId],
36
+ queryFn: async () => {
37
+ const matchingGenes = await getEdges(item, 'gene');
38
+ return matchingGenes.map(d => ({ target: d, source: item }));
39
+ },
40
+ })) || [],
41
+ });
42
+
43
+ const anyLoading = queries.some(q => q.isFetching);
44
+ const anyError = queries.some(q => q.isError);
45
+ // eslint-disable-next-line no-nested-ternary
46
+ const dataStatus = anyLoading ? 'loading' : (anyError ? 'error' : 'success');
47
+
48
+ const data = queries
49
+ .flatMap(q => q.data)
50
+ .filter(Boolean);
51
+
52
+ const rows = useMemo(() => data.map(d => ({
53
+ id: d.target.kgId,
54
+ targetLabel: d.target.label,
55
+ sourceRelationship: (d.source.nodeType === 'gene'
56
+ ? 'Directly-selected gene'
57
+ : `Member of ${d.source.label} ${d.source.nodeType}`
58
+ ),
59
+ // eslint-disable-next-line react-hooks/exhaustive-deps
60
+ })), [currentModalityAgnosticSelection, dataStatus]);
61
+
62
+ // Derive the set of selected row ids from the
63
+ // currentModalitySpecificSelection.
64
+ const rowSelectionModel = useMemo(
65
+ () => currentModalitySpecificSelection?.map(d => d.kgId) || [],
66
+ [rows, currentModalitySpecificSelection],
67
+ );
68
+
69
+ function handleSelection(newRowSelectionModel) {
70
+ const newSelection = newRowSelectionModel
71
+ .map(kgId => data.find(d => d.target.kgId === kgId)?.target)
72
+ .filter(Boolean);
73
+ setCurrentModalitySpecificSelection(newSelection);
74
+ }
75
+
76
+ return (
77
+ <>
78
+ <Grid item xs={6}>
79
+ <Typography variant="h6">Select a feature type:</Typography>
80
+ </Grid>
81
+ <Grid item container xs={6}>
82
+ <Grid item>
83
+ <FormControl fullWidth>
84
+ <Select
85
+ native
86
+ defaultValue="gene"
87
+ classes={{ select: classes.selectInput }}
88
+ inputProps={{
89
+ name: 'age',
90
+ id: 'uncontrolled-native',
91
+ }}
92
+ >
93
+ <option value="gene">Gene (RNA-seq)</option>
94
+ </Select>
95
+ <FormHelperText>Feature type (experimental modality)</FormHelperText>
96
+ </FormControl>
97
+ </Grid>
98
+ </Grid>
99
+ <Grid item container xs={12} style={{ height: '450px' }}>
100
+ <DataGrid
101
+ rows={rows}
102
+ columns={columns}
103
+ pageSize={10}
104
+ checkboxSelection
105
+ onSelectionModelChange={handleSelection}
106
+ rowSelectionModel={rowSelectionModel}
107
+ />
108
+ </Grid>
109
+ </>
110
+ );
111
+ }
@@ -0,0 +1,90 @@
1
+ import React from 'react';
2
+ import { Grid, RadioGroup, FormControl, FormLabel, Radio, FormControlLabel, Typography } from '@material-ui/core';
3
+
4
+
5
+ export function SelectStratification(props) {
6
+ const {
7
+ stratifications,
8
+ currentStratificationSelection: currentStratificationSelectionProp,
9
+ setCurrentStratificationSelection: setCurrentStratificationSelectionProp,
10
+ } = props;
11
+
12
+ // Convert back and forth from the special value '__all__' to null.
13
+ const currentStratificationSelection = currentStratificationSelectionProp === null
14
+ ? '__all__'
15
+ : currentStratificationSelectionProp.stratificationId;
16
+ function setCurrentStratificationSelection(event, value) {
17
+ setCurrentStratificationSelectionProp(
18
+ stratifications.find(s => s.stratificationId === value) ?? null,
19
+ );
20
+ }
21
+
22
+ const sampleSetOptions = stratifications?.filter(s => s.stratificationType === 'sampleSet');
23
+ const structuralRegionOptions = stratifications?.filter(s => s.stratificationType === 'structural-region');
24
+
25
+ const hasSampleSetOptions = sampleSetOptions && sampleSetOptions.length > 0;
26
+ const hasStructuralRegionOptions = structuralRegionOptions && structuralRegionOptions.length > 0;
27
+
28
+ return (
29
+ <Grid item xs={12}>
30
+ <Typography variant="h6">
31
+ Select a stratification option:
32
+ </Typography>
33
+ <FormControl>
34
+ <RadioGroup
35
+ aria-labelledby="demo-radio-buttons-group-label"
36
+ value={currentStratificationSelection}
37
+ onChange={setCurrentStratificationSelection}
38
+ name="radio-buttons-group"
39
+ >
40
+ <FormControlLabel value="__all__" control={<Radio />} label="No stratification" />
41
+ {hasSampleSetOptions ? (
42
+ <>
43
+ <FormLabel>By participant group (clinical):</FormLabel>
44
+ {sampleSetOptions.map(s => (
45
+ <FormControlLabel
46
+ value={s.stratificationId}
47
+ control={<Radio />}
48
+ key={s.name}
49
+ label={s.name}
50
+ />
51
+ ))}
52
+ </>
53
+ ) : null}
54
+ {hasStructuralRegionOptions ? (
55
+ <>
56
+ <FormLabel>By segmentation-defined spatial region (structural):</FormLabel>
57
+ {structuralRegionOptions.map(s => (
58
+ <FormControlLabel
59
+ value={s.stratificationId}
60
+ control={<Radio />}
61
+ key={s.name}
62
+ label={s.name}
63
+ />
64
+ ))}
65
+ </>
66
+ ) : null}
67
+ </RadioGroup>
68
+ </FormControl>
69
+ {/* // TODO: store participant-level and region-level stratification options independently
70
+ <Typography variant="h6">
71
+ Define case-control stratification (region-level):
72
+ </Typography>
73
+ <FormControl>
74
+ <RadioGroup
75
+ aria-labelledby="demo-radio-buttons-group-label-2"
76
+ value={currentStratificationSelection}
77
+ onChange={setCurrentStratificationSelection}
78
+ name="radio-buttons-group-2"
79
+ >
80
+ <FormLabel>By segmentation-defined spatial region (structural):</FormLabel>
81
+ {stratifications ? stratifications
82
+ .filter(s => s.stratificationType === 'structural-region').map((s) => (
83
+ <FormControlLabel value={s.stratificationId} control={<Radio />} label={s.name} />
84
+ )) : null}
85
+ </RadioGroup>
86
+ </FormControl>
87
+ */}
88
+ </Grid>
89
+ );
90
+ }
package/src/styles.js ADDED
@@ -0,0 +1,33 @@
1
+ import { makeStyles } from '@material-ui/core';
2
+
3
+ export const useStyles = makeStyles(() => ({
4
+ header: {
5
+ marginTop: '10px',
6
+ marginBottom: '10px',
7
+ },
8
+ fullWidthBox: {
9
+ width: '100%',
10
+ },
11
+ selectButton: {
12
+ position: 'absolute',
13
+ right: 0,
14
+ },
15
+ accordion: {
16
+ width: '100%',
17
+ margin: '0 !important',
18
+ },
19
+ accordionDetails: {
20
+ paddingTop: 0,
21
+ },
22
+ cartUl: {
23
+ marginTop: 0,
24
+ marginBottom: 0,
25
+ },
26
+ selectInput: {
27
+ height: 'auto !important',
28
+ },
29
+ searchInput: {
30
+ lineHeight: 'initial',
31
+ height: 'auto !important',
32
+ },
33
+ }));