@vitessce/biomarker-select 3.5.9 → 3.5.11

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.
@@ -1 +1 @@
1
- {"version":3,"file":"ComparativeHeadingSubscriber.d.ts","sourceRoot":"","sources":["../src/ComparativeHeadingSubscriber.js"],"names":[],"mappings":"AA+BA,sEAuDC"}
1
+ {"version":3,"file":"ComparativeHeadingSubscriber.d.ts","sourceRoot":"","sources":["../src/ComparativeHeadingSubscriber.js"],"names":[],"mappings":"AAsCA,sEA8EC"}
@@ -11,6 +11,13 @@ const useStyles = makeStyles(theme => ({
11
11
  flexDirection: 'column',
12
12
  justifyContent: 'center',
13
13
  height: '100%',
14
+ '& div': {
15
+ display: 'flex',
16
+ flexDirection: 'row',
17
+ },
18
+ '& div > button': {
19
+ height: 'auto',
20
+ },
14
21
  },
15
22
  headingContainer: {
16
23
  backgroundColor: 'transparent',
@@ -28,10 +35,7 @@ const useStyles = makeStyles(theme => ({
28
35
  }));
29
36
  export function ComparativeHeadingSubscriber(props) {
30
37
  const { coordinationScopes, } = props;
31
- const [{ sampleSetSelection, featureSelection, }, { setSampleSetSelection,
32
- // TODO: buttons for modification of featureSelection
33
- // setFeatureSelection,
34
- }] = useCoordination(COMPONENT_COORDINATION_TYPES[ViewType.COMPARATIVE_HEADING], coordinationScopes);
38
+ const [{ sampleSetSelection, featureSelection, }, { setSampleSetSelection, setFeatureSelection, }] = useCoordination(COMPONENT_COORDINATION_TYPES[ViewType.COMPARATIVE_HEADING], coordinationScopes);
35
39
  const classes = useStyles();
36
40
  const swapSampleSets = useCallback(() => {
37
41
  if (sampleSetSelection?.length === 2) {
@@ -41,5 +45,15 @@ export function ComparativeHeadingSubscriber(props) {
41
45
  ]);
42
46
  }
43
47
  }, [sampleSetSelection, setSampleSetSelection]);
44
- return (_jsxs("div", { className: classes.headingContainer, children: [sampleSetSelection && sampleSetSelection.length === 2 ? (_jsxs("div", { className: classes.headingSectionContainer, children: [_jsx("div", { style: { width: '45%' }, children: _jsx("h2", { className: classes.headingText, children: sampleSetSelection?.[0]?.at(-1) }) }), _jsx("div", { style: { width: '5%' }, children: _jsx("h2", { className: classes.headingText, style: { textAlign: 'right' }, children: "vs.\u00A0" }) }), _jsx("div", { style: { width: '50%' }, children: _jsx("h2", { className: classes.headingText, children: sampleSetSelection?.[1]?.at(-1) }) }), _jsx("div", { className: classes.buttonContainer, children: _jsx("button", { onClick: swapSampleSets, type: "button", children: "Swap" }) })] })) : null, featureSelection && featureSelection.length > 0 ? (_jsxs("div", { className: classes.headingSectionContainer, children: [_jsx("div", { children: _jsx("h3", { className: classes.headingText, children: "Selected Biomarkers:\u00A0" }) }), featureSelection?.map((featureName, i) => (_jsx("div", { children: _jsxs("h3", { className: classes.headingText, children: [featureName, i < featureSelection.length - 1 ? (_jsx("span", { children: ",\u00A0" })) : null] }) })))] })) : null] }));
48
+ const clearSampleSetSelection = useCallback(() => {
49
+ if (sampleSetSelection?.length === 2) {
50
+ setSampleSetSelection(null);
51
+ }
52
+ }, [sampleSetSelection, setSampleSetSelection]);
53
+ const clearFeatureSelection = useCallback(() => {
54
+ if (Array.isArray(featureSelection) && featureSelection.length > 0) {
55
+ setFeatureSelection(null);
56
+ }
57
+ }, [featureSelection, setFeatureSelection]);
58
+ return (_jsxs("div", { className: classes.headingContainer, children: [sampleSetSelection && sampleSetSelection.length === 2 ? (_jsxs("div", { className: classes.headingSectionContainer, children: [_jsx("div", { style: { width: '45%' }, children: _jsx("h2", { className: classes.headingText, children: sampleSetSelection?.[0]?.at(-1) }) }), _jsx("div", { style: { width: '5%' }, children: _jsx("h2", { className: classes.headingText, style: { textAlign: 'right' }, children: "vs.\u00A0" }) }), _jsx("div", { style: { width: '50%' }, children: _jsx("h2", { className: classes.headingText, children: sampleSetSelection?.[1]?.at(-1) }) }), _jsx("div", { className: classes.buttonContainer, children: _jsxs("div", { children: [_jsx("button", { onClick: swapSampleSets, type: "button", children: "Swap" }), _jsx("button", { onClick: clearSampleSetSelection, type: "button", children: "Clear" })] }) })] })) : (_jsx("div", { className: classes.headingSectionContainer, children: _jsx("div", { children: _jsx("h2", { className: classes.headingText, children: "All samples" }) }) })), featureSelection && featureSelection.length > 0 ? (_jsxs("div", { className: classes.headingSectionContainer, children: [_jsx("div", { children: _jsx("h3", { className: classes.headingText, children: "Selected Biomarkers:\u00A0" }) }), featureSelection?.map((featureName, i) => (_jsx("div", { children: _jsxs("h3", { className: classes.headingText, children: [featureName, i < featureSelection.length - 1 ? (_jsx("span", { children: ",\u00A0" })) : null] }) }, featureName))), _jsx("div", { className: classes.buttonContainer, children: _jsx("div", { children: _jsx("button", { onClick: clearFeatureSelection, type: "button", children: "Clear" }) }) })] })) : null] }));
45
59
  }
@@ -0,0 +1,2 @@
1
+ export function SampleSetPairManagerSubscriber(props: any): JSX.Element;
2
+ //# sourceMappingURL=SampleSetPairManagerSubscriber.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SampleSetPairManagerSubscriber.d.ts","sourceRoot":"","sources":["../src/SampleSetPairManagerSubscriber.js"],"names":[],"mappings":"AA6BA,wEAoHC"}
@@ -0,0 +1,76 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useMemo, useCallback } from 'react';
3
+ import { isEqual } from 'lodash-es';
4
+ import clsx from 'clsx';
5
+ import { makeStyles } from '@material-ui/core';
6
+ import { TitleInfo, useReady, useCoordination, useLoaders, useComparisonMetadata, useMatchingLoader, useColumnNameMapping, } from '@vitessce/vit-s';
7
+ import { ViewType, DataType, COMPONENT_COORDINATION_TYPES, ViewHelpMapping } from '@vitessce/constants-internal';
8
+ const useStyles = makeStyles(() => ({
9
+ selectedPair: {
10
+ fontWeight: 'bold',
11
+ },
12
+ pairUl: {
13
+ paddingLeft: '15px',
14
+ marginTop: 0,
15
+ fontSize: '12px',
16
+ '& button': {
17
+ fontSize: '12px',
18
+ },
19
+ },
20
+ }));
21
+ export function SampleSetPairManagerSubscriber(props) {
22
+ const { coordinationScopes, removeGridComponent, theme, title = 'Sample Sets', closeButtonVisible, helpText = ViewHelpMapping.SAMPLE_SET_PAIR_MANAGER, } = props;
23
+ const classes = useStyles();
24
+ const loaders = useLoaders();
25
+ // Get "props" from the coordination space.
26
+ const [{ dataset, obsType, sampleType, sampleSetSelection, }, { setSampleSetSelection, }] = useCoordination(COMPONENT_COORDINATION_TYPES[ViewType.SAMPLE_SET_PAIR_MANAGER], coordinationScopes);
27
+ const sampleSetsLoader = useMatchingLoader(loaders, dataset, DataType.SAMPLE_SETS, { sampleType });
28
+ const sampleSetsColumnNameMappingReversed = useColumnNameMapping(sampleSetsLoader, true);
29
+ const [{ comparisonMetadata }, cmpMetadataStatus] = useComparisonMetadata(loaders, dataset, false, {}, {}, { obsType, sampleType });
30
+ const isReady = useReady([
31
+ cmpMetadataStatus,
32
+ ]);
33
+ const stratificationOptions = useMemo(() => {
34
+ /*
35
+ return array of objects like {
36
+ stratificationId: 'aki-vs-hr',
37
+ name: 'Acute kidney injury (AKI) vs. Healthy reference',
38
+ stratificationType: 'sampleSet',
39
+ sampleSets: [
40
+ ['Disease Type', 'AKI'],
41
+ ['Disease Type', 'Reference'],
42
+ ],
43
+ },
44
+ */
45
+ if (comparisonMetadata?.sample_group_pairs) {
46
+ return comparisonMetadata.sample_group_pairs.map((sampleGroupPair) => {
47
+ const [sampleGroupCol, sampleGroupValues] = sampleGroupPair;
48
+ const [sampleGroupCtrl, sampleGroupCase] = sampleGroupValues;
49
+ const groupName = sampleSetsColumnNameMappingReversed?.[sampleGroupCol];
50
+ return {
51
+ stratificationId: `${sampleGroupCol}_${sampleGroupCtrl}-vs-${sampleGroupCase}`,
52
+ name: `${groupName}: ${sampleGroupCtrl} vs. ${sampleGroupCase}`,
53
+ stratificationType: 'sampleSet',
54
+ sampleSets: [
55
+ // With sampleSets coming from the comparison_metadata,
56
+ // need to use loader options from obsSets and sampleSets to get mapping
57
+ // from column name to group name.
58
+ [groupName, sampleGroupCtrl],
59
+ [groupName, sampleGroupCase],
60
+ ],
61
+ };
62
+ });
63
+ }
64
+ return null;
65
+ }, [comparisonMetadata, sampleSetsColumnNameMappingReversed]);
66
+ const onSelectPair = useCallback((sampleSetPair) => {
67
+ setSampleSetSelection(sampleSetPair);
68
+ }, [sampleSetSelection, setSampleSetSelection]);
69
+ return (_jsx(TitleInfo, { title: title, closeButtonVisible: closeButtonVisible, removeGridComponent: removeGridComponent, isScroll: true, theme: theme, isReady: isReady, helpText: helpText, children: _jsx("ul", { className: classes.pairUl, children: stratificationOptions?.map((pairObj) => {
70
+ const isSelected = Array.isArray(sampleSetSelection)
71
+ && sampleSetSelection.length === 2
72
+ && (isEqual(pairObj.sampleSets, sampleSetSelection)
73
+ || isEqual(pairObj.sampleSets, [sampleSetSelection?.[1], sampleSetSelection?.[0]]));
74
+ return (_jsxs("li", { className: clsx({ [classes.selectedPair]: isSelected }), children: [pairObj.name, "\u00A0", !isSelected ? (_jsx("button", { type: "button", onClick: () => onSelectPair(pairObj.sampleSets), children: "Select" })) : null] }, pairObj.stratificationId));
75
+ }) }) }));
76
+ }
@@ -13,13 +13,13 @@ export function autocompleteFeature({ queryClient }: {
13
13
  * @satisfies {TransformFeatureFunc}
14
14
  * @param {object} ctx
15
15
  * @param {QueryClient} ctx.queryClient
16
- * @param {KgNode} node
16
+ * @param {KgNode} nodeOrig
17
17
  * @param {TargetModalityType} targetModality
18
18
  * @returns {Promise<KgNode[]>}
19
19
  */
20
20
  export function transformFeature({ queryClient }: {
21
21
  queryClient: QueryClient;
22
- }, node: KgNode, targetModality: TargetModalityType): Promise<KgNode[]>;
22
+ }, nodeOrig: KgNode, targetModality: TargetModalityType): Promise<KgNode[]>;
23
23
  /**
24
24
  * @satisfies {GetAlternativeTermsFunc}
25
25
  * @param {object} ctx
@@ -1 +1 @@
1
- {"version":3,"file":"default-async-functions.d.ts","sourceRoot":"","sources":["../src/default-async-functions.js"],"names":[],"mappings":"AAqHA;;;;;;;GAOG;AACH,qDALG;IAAyB,WAAW,EAA5B,WAAW;CACnB,WAAQ,MAAM,kBACN,IAAI,GAAG,kBAAkB,GACvB,OAAO,CAAC,MAAM,EAAE,CAAC,CAiB7B;AAeD;;;;;;;GAOG;AACH,kDALG;IAAyB,WAAW,EAA5B,WAAW;CACnB,QAAQ,MAAM,kBACN,kBAAkB,GAChB,OAAO,CAAC,MAAM,EAAE,CAAC,CAmC7B;AAED;;;;;;GAMG;AACH,qDAJG;IAAyB,WAAW,EAA5B,WAAW;CACnB,SAAQ,MAAM,GACJ,OAAO,CAAC,MAAM,EAAE,CAAC,CA2B7B;AAED;;;;;;;GAOG;AACH,gDALG;IAAyB,WAAW,EAA5B,WAAW;CACnB,kBAAQ,MAAM,kBACN,MAAM,GACJ,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAoBxC;iCA1PsD,uBAAuB;wCADsE,iBAAiB;4BAAjB,iBAAiB"}
1
+ {"version":3,"file":"default-async-functions.d.ts","sourceRoot":"","sources":["../src/default-async-functions.js"],"names":[],"mappings":"AAyHA;;;;;;;GAOG;AACH,qDALG;IAAyB,WAAW,EAA5B,WAAW;CACnB,WAAQ,MAAM,kBACN,IAAI,GAAG,kBAAkB,GACvB,OAAO,CAAC,MAAM,EAAE,CAAC,CAiB7B;AAkBD;;;;;;;GAOG;AACH,kDALG;IAAyB,WAAW,EAA5B,WAAW;CACnB,YAAQ,MAAM,kBACN,kBAAkB,GAChB,OAAO,CAAC,MAAM,EAAE,CAAC,CAoD7B;AAED;;;;;;GAMG;AACH,qDAJG;IAAyB,WAAW,EAA5B,WAAW;CACnB,SAAQ,MAAM,GACJ,OAAO,CAAC,MAAM,EAAE,CAAC,CA2B7B;AAED;;;;;;;GAOG;AACH,gDALG;IAAyB,WAAW,EAA5B,WAAW;CACnB,kBAAQ,MAAM,kBACN,MAAM,GACJ,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAoBxC;iCAlRsD,uBAAuB;wCADsE,iBAAiB;4BAAjB,iBAAiB"}
@@ -44,14 +44,18 @@ function loadCellTypeNodes() {
44
44
  * @returns {Promise<KgNode[]>}
45
45
  */
46
46
  function loadPathwayNodes() {
47
- return fetch(`${KG_BASE_URL}/Reactome_2022.nodes.csv`)
48
- .then(res => res.text())
47
+ const reactomeNodes = fetch(`${KG_BASE_URL}/Reactome_2022.nodes.csv`);
48
+ // TODO: load both GO and Reactome nodes, concat together.
49
+ // const goNodes = fetch(`${KG_BASE_URL}/GO_Biological_Process_2021.nodes.csv`);
50
+ return reactomeNodes.then(res => res.text())
49
51
  .then((res) => {
50
52
  const result = csvParse(res);
51
53
  return result.map((/** @type {any} */ d) => ({
52
54
  kgId: d.id,
53
- label: d.pathway,
54
- term: `reactome:${d.acc}`,
55
+ label: d.pathway, // For reactome
56
+ term: `REACTOME:${d.acc}`, // For reactome
57
+ // label: d.ontology_label, // For GO_BP
58
+ // term: d.acc, // For GO_BP
55
59
  nodeType: 'pathway',
56
60
  }));
57
61
  });
@@ -124,7 +128,10 @@ export async function autocompleteFeature({ queryClient }, partial, targetModali
124
128
  return results.map((/** @type {any} */ result) => result.item);
125
129
  }
126
130
  async function loadPathwayToGeneEdges() {
127
- return fetch(`${KG_BASE_URL}/Reactome_2022.Reactome.Gene.edges.csv`)
131
+ const reactomeEdges = fetch(`${KG_BASE_URL}/Reactome_2022.Reactome.Gene.edges.csv`);
132
+ // TODO: load both GO and Reactome edges, concat together.
133
+ // const goEdges = fetch(`${KG_BASE_URL}/GO_Biological_Process_2021.GO_BP.Gene.edges.csv`);
134
+ return reactomeEdges
128
135
  .then(res => res.text())
129
136
  .then((res) => {
130
137
  const result = csvParse(res);
@@ -139,11 +146,14 @@ async function loadPathwayToGeneEdges() {
139
146
  * @satisfies {TransformFeatureFunc}
140
147
  * @param {object} ctx
141
148
  * @param {QueryClient} ctx.queryClient
142
- * @param {KgNode} node
149
+ * @param {KgNode} nodeOrig
143
150
  * @param {TargetModalityType} targetModality
144
151
  * @returns {Promise<KgNode[]>}
145
152
  */
146
- export async function transformFeature({ queryClient }, node, targetModality) {
153
+ export async function transformFeature({ queryClient }, nodeOrig, targetModality) {
154
+ const node = {
155
+ ...nodeOrig,
156
+ };
147
157
  if (targetModality === node.nodeType) {
148
158
  // For example, if the target modality is gene and the node is already a gene node.
149
159
  return [node];
@@ -161,6 +171,20 @@ export async function transformFeature({ queryClient }, node, targetModality) {
161
171
  staleTime: Infinity,
162
172
  queryFn: loadPathwayToGeneEdges,
163
173
  });
174
+ if (!node.kgId) {
175
+ const pathwayNodes = await queryClient.fetchQuery({
176
+ queryKey: ['pathwayNodes'],
177
+ staleTime: Infinity,
178
+ queryFn: loadPathwayNodes,
179
+ });
180
+ const foundId = pathwayNodes.find(n => n.term === node.term)?.kgId;
181
+ if (foundId) {
182
+ node.kgId = foundId;
183
+ }
184
+ else {
185
+ console.warn('Could not find matching pathway node based on term.');
186
+ }
187
+ }
164
188
  // TODO: support matching using ontology term (rather than requiring kgId)?
165
189
  const matchingEdges = pathwayGeneEdges.filter((/** @type {KgEdge} */ d) => d.source === node.kgId);
166
190
  const matchingGeneIds = matchingEdges.map((/** @type {KgEdge} */ d) => d.target);
@@ -1,4 +1,5 @@
1
1
  export { BiomarkerSelectSubscriber } from "./BiomarkerSelectSubscriber.js";
2
2
  export { ComparativeHeadingSubscriber } from "./ComparativeHeadingSubscriber.js";
3
+ export { SampleSetPairManagerSubscriber } from "./SampleSetPairManagerSubscriber.js";
3
4
  export { autocompleteFeature, transformFeature, getAlternativeTerms, getTermMapping } from "./default-async-functions.js";
4
5
  //# sourceMappingURL=index.d.ts.map
package/dist-tsc/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { BiomarkerSelectSubscriber } from './BiomarkerSelectSubscriber.js';
2
2
  export { ComparativeHeadingSubscriber } from './ComparativeHeadingSubscriber.js';
3
+ export { SampleSetPairManagerSubscriber } from './SampleSetPairManagerSubscriber.js';
3
4
  export { autocompleteFeature, transformFeature, getAlternativeTerms, getTermMapping, } from './default-async-functions.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitessce/biomarker-select",
3
- "version": "3.5.9",
3
+ "version": "3.5.11",
4
4
  "author": "Gehlenborg Lab",
5
5
  "homepage": "http://vitessce.io",
6
6
  "repository": {
@@ -24,9 +24,10 @@
24
24
  "react-window": "^1.8.10",
25
25
  "fuse.js": "^7.0.0",
26
26
  "d3-dsv": "^1.1.1",
27
- "@vitessce/constants-internal": "3.5.9",
28
- "@vitessce/vit-s": "3.5.9",
29
- "@vitessce/types": "3.5.9"
27
+ "clsx": "^1.1.1",
28
+ "@vitessce/constants-internal": "3.5.11",
29
+ "@vitessce/vit-s": "3.5.11",
30
+ "@vitessce/types": "3.5.11"
30
31
  },
31
32
  "devDependencies": {
32
33
  "@testing-library/jest-dom": "^5.16.4",
@@ -13,6 +13,13 @@ const useStyles = makeStyles(theme => ({
13
13
  flexDirection: 'column',
14
14
  justifyContent: 'center',
15
15
  height: '100%',
16
+ '& div': {
17
+ display: 'flex',
18
+ flexDirection: 'row',
19
+ },
20
+ '& div > button': {
21
+ height: 'auto',
22
+ },
16
23
  },
17
24
  headingContainer: {
18
25
  backgroundColor: 'transparent',
@@ -39,8 +46,7 @@ export function ComparativeHeadingSubscriber(props) {
39
46
  featureSelection,
40
47
  }, {
41
48
  setSampleSetSelection,
42
- // TODO: buttons for modification of featureSelection
43
- // setFeatureSelection,
49
+ setFeatureSelection,
44
50
  }] = useCoordination(
45
51
  COMPONENT_COORDINATION_TYPES[ViewType.COMPARATIVE_HEADING],
46
52
  coordinationScopes,
@@ -57,6 +63,18 @@ export function ComparativeHeadingSubscriber(props) {
57
63
  }
58
64
  }, [sampleSetSelection, setSampleSetSelection]);
59
65
 
66
+ const clearSampleSetSelection = useCallback(() => {
67
+ if (sampleSetSelection?.length === 2) {
68
+ setSampleSetSelection(null);
69
+ }
70
+ }, [sampleSetSelection, setSampleSetSelection]);
71
+
72
+ const clearFeatureSelection = useCallback(() => {
73
+ if (Array.isArray(featureSelection) && featureSelection.length > 0) {
74
+ setFeatureSelection(null);
75
+ }
76
+ }, [featureSelection, setFeatureSelection]);
77
+
60
78
  return (
61
79
  <div className={classes.headingContainer}>
62
80
  {sampleSetSelection && sampleSetSelection.length === 2 ? (
@@ -65,21 +83,33 @@ export function ComparativeHeadingSubscriber(props) {
65
83
  <div style={{ width: '5%' }}><h2 className={classes.headingText} style={{ textAlign: 'right' }}>vs.&nbsp;</h2></div>
66
84
  <div style={{ width: '50%' }}><h2 className={classes.headingText}>{sampleSetSelection?.[1]?.at(-1)}</h2></div>
67
85
  <div className={classes.buttonContainer}>
68
- <button onClick={swapSampleSets} type="button">Swap</button>
86
+ <div>
87
+ <button onClick={swapSampleSets} type="button">Swap</button>
88
+ <button onClick={clearSampleSetSelection} type="button">Clear</button>
89
+ </div>
69
90
  </div>
70
91
  </div>
71
- ) : null}
92
+ ) : (
93
+ <div className={classes.headingSectionContainer}>
94
+ <div><h2 className={classes.headingText}>All samples</h2></div>
95
+ </div>
96
+ )}
72
97
  {featureSelection && featureSelection.length > 0 ? (
73
98
  <div className={classes.headingSectionContainer}>
74
99
  <div><h3 className={classes.headingText}>Selected Biomarkers:&nbsp;</h3></div>
75
100
  {featureSelection?.map((featureName, i) => (
76
- <div>
101
+ <div key={featureName}>
77
102
  <h3 className={classes.headingText}>
78
103
  {featureName}
79
104
  {i < featureSelection.length - 1 ? (<span>,&nbsp;</span>) : null}
80
105
  </h3>
81
106
  </div>
82
107
  ))}
108
+ <div className={classes.buttonContainer}>
109
+ <div>
110
+ <button onClick={clearFeatureSelection} type="button">Clear</button>
111
+ </div>
112
+ </div>
83
113
  </div>
84
114
  ) : null}
85
115
  </div>
@@ -0,0 +1,146 @@
1
+ import React, { useMemo, useCallback } from 'react';
2
+ import { isEqual } from 'lodash-es';
3
+ import clsx from 'clsx';
4
+ import { makeStyles } from '@material-ui/core';
5
+ import {
6
+ TitleInfo,
7
+ useReady,
8
+ useCoordination,
9
+ useLoaders,
10
+ useComparisonMetadata,
11
+ useMatchingLoader,
12
+ useColumnNameMapping,
13
+ } from '@vitessce/vit-s';
14
+ import { ViewType, DataType, COMPONENT_COORDINATION_TYPES, ViewHelpMapping } from '@vitessce/constants-internal';
15
+
16
+ const useStyles = makeStyles(() => ({
17
+ selectedPair: {
18
+ fontWeight: 'bold',
19
+ },
20
+ pairUl: {
21
+ paddingLeft: '15px',
22
+ marginTop: 0,
23
+ fontSize: '12px',
24
+ '& button': {
25
+ fontSize: '12px',
26
+ },
27
+ },
28
+ }));
29
+
30
+ export function SampleSetPairManagerSubscriber(props) {
31
+ const {
32
+ coordinationScopes,
33
+ removeGridComponent,
34
+ theme,
35
+ title = 'Sample Sets',
36
+ closeButtonVisible,
37
+ helpText = ViewHelpMapping.SAMPLE_SET_PAIR_MANAGER,
38
+ } = props;
39
+
40
+ const classes = useStyles();
41
+ const loaders = useLoaders();
42
+
43
+ // Get "props" from the coordination space.
44
+ const [{
45
+ dataset,
46
+ obsType,
47
+ sampleType,
48
+ sampleSetSelection,
49
+ }, {
50
+ setSampleSetSelection,
51
+ }] = useCoordination(
52
+ COMPONENT_COORDINATION_TYPES[ViewType.SAMPLE_SET_PAIR_MANAGER],
53
+ coordinationScopes,
54
+ );
55
+
56
+ const sampleSetsLoader = useMatchingLoader(
57
+ loaders, dataset, DataType.SAMPLE_SETS, { sampleType },
58
+ );
59
+ const sampleSetsColumnNameMappingReversed = useColumnNameMapping(sampleSetsLoader, true);
60
+
61
+ const [{ comparisonMetadata }, cmpMetadataStatus] = useComparisonMetadata(
62
+ loaders, dataset, false, {}, {}, { obsType, sampleType },
63
+ );
64
+
65
+ const isReady = useReady([
66
+ cmpMetadataStatus,
67
+ ]);
68
+
69
+ const stratificationOptions = useMemo(() => {
70
+ /*
71
+ return array of objects like {
72
+ stratificationId: 'aki-vs-hr',
73
+ name: 'Acute kidney injury (AKI) vs. Healthy reference',
74
+ stratificationType: 'sampleSet',
75
+ sampleSets: [
76
+ ['Disease Type', 'AKI'],
77
+ ['Disease Type', 'Reference'],
78
+ ],
79
+ },
80
+ */
81
+ if (comparisonMetadata?.sample_group_pairs) {
82
+ return comparisonMetadata.sample_group_pairs.map((sampleGroupPair) => {
83
+ const [sampleGroupCol, sampleGroupValues] = sampleGroupPair;
84
+ const [sampleGroupCtrl, sampleGroupCase] = sampleGroupValues;
85
+ const groupName = sampleSetsColumnNameMappingReversed?.[sampleGroupCol];
86
+ return {
87
+ stratificationId: `${sampleGroupCol}_${sampleGroupCtrl}-vs-${sampleGroupCase}`,
88
+ name: `${groupName}: ${sampleGroupCtrl} vs. ${sampleGroupCase}`,
89
+ stratificationType: 'sampleSet',
90
+ sampleSets: [
91
+ // With sampleSets coming from the comparison_metadata,
92
+ // need to use loader options from obsSets and sampleSets to get mapping
93
+ // from column name to group name.
94
+ [groupName, sampleGroupCtrl],
95
+ [groupName, sampleGroupCase],
96
+ ],
97
+ };
98
+ });
99
+ }
100
+ return null;
101
+ }, [comparisonMetadata, sampleSetsColumnNameMappingReversed]);
102
+
103
+ const onSelectPair = useCallback((sampleSetPair) => {
104
+ setSampleSetSelection(sampleSetPair);
105
+ }, [sampleSetSelection, setSampleSetSelection]);
106
+
107
+
108
+ return (
109
+ <TitleInfo
110
+ title={title}
111
+ closeButtonVisible={closeButtonVisible}
112
+ removeGridComponent={removeGridComponent}
113
+ isScroll
114
+ theme={theme}
115
+ isReady={isReady}
116
+ helpText={helpText}
117
+ >
118
+ <ul className={classes.pairUl}>
119
+ {stratificationOptions?.map((pairObj) => {
120
+ const isSelected = Array.isArray(sampleSetSelection)
121
+ && sampleSetSelection.length === 2
122
+ && (
123
+ isEqual(pairObj.sampleSets, sampleSetSelection)
124
+ || isEqual(pairObj.sampleSets, [sampleSetSelection?.[1], sampleSetSelection?.[0]])
125
+ );
126
+ return (
127
+ <li
128
+ key={pairObj.stratificationId}
129
+ className={clsx({ [classes.selectedPair]: isSelected })}
130
+ >
131
+ {pairObj.name}&nbsp;
132
+ {!isSelected ? (
133
+ <button
134
+ type="button"
135
+ onClick={() => onSelectPair(pairObj.sampleSets)}
136
+ >
137
+ Select
138
+ </button>
139
+ ) : null}
140
+ </li>
141
+ );
142
+ })}
143
+ </ul>
144
+ </TitleInfo>
145
+ );
146
+ }
@@ -49,14 +49,18 @@ function loadCellTypeNodes() {
49
49
  * @returns {Promise<KgNode[]>}
50
50
  */
51
51
  function loadPathwayNodes() {
52
- return fetch(`${KG_BASE_URL}/Reactome_2022.nodes.csv`)
53
- .then(res => res.text())
52
+ const reactomeNodes = fetch(`${KG_BASE_URL}/Reactome_2022.nodes.csv`);
53
+ // TODO: load both GO and Reactome nodes, concat together.
54
+ // const goNodes = fetch(`${KG_BASE_URL}/GO_Biological_Process_2021.nodes.csv`);
55
+ return reactomeNodes.then(res => res.text())
54
56
  .then((res) => {
55
57
  const result = csvParse(res);
56
58
  return result.map((/** @type {any} */ d) => ({
57
59
  kgId: d.id,
58
- label: d.pathway,
59
- term: `reactome:${d.acc}`,
60
+ label: d.pathway, // For reactome
61
+ term: `REACTOME:${d.acc}`, // For reactome
62
+ // label: d.ontology_label, // For GO_BP
63
+ // term: d.acc, // For GO_BP
60
64
  nodeType: 'pathway',
61
65
  }));
62
66
  });
@@ -141,7 +145,10 @@ export async function autocompleteFeature({ queryClient }, partial, targetModali
141
145
  }
142
146
 
143
147
  async function loadPathwayToGeneEdges() {
144
- return fetch(`${KG_BASE_URL}/Reactome_2022.Reactome.Gene.edges.csv`)
148
+ const reactomeEdges = fetch(`${KG_BASE_URL}/Reactome_2022.Reactome.Gene.edges.csv`);
149
+ // TODO: load both GO and Reactome edges, concat together.
150
+ // const goEdges = fetch(`${KG_BASE_URL}/GO_Biological_Process_2021.GO_BP.Gene.edges.csv`);
151
+ return reactomeEdges
145
152
  .then(res => res.text())
146
153
  .then((res) => {
147
154
  const result = csvParse(res);
@@ -157,11 +164,14 @@ async function loadPathwayToGeneEdges() {
157
164
  * @satisfies {TransformFeatureFunc}
158
165
  * @param {object} ctx
159
166
  * @param {QueryClient} ctx.queryClient
160
- * @param {KgNode} node
167
+ * @param {KgNode} nodeOrig
161
168
  * @param {TargetModalityType} targetModality
162
169
  * @returns {Promise<KgNode[]>}
163
170
  */
164
- export async function transformFeature({ queryClient }, node, targetModality) {
171
+ export async function transformFeature({ queryClient }, nodeOrig, targetModality) {
172
+ const node = {
173
+ ...nodeOrig,
174
+ };
165
175
  if (targetModality === node.nodeType) {
166
176
  // For example, if the target modality is gene and the node is already a gene node.
167
177
  return [node];
@@ -180,6 +190,20 @@ export async function transformFeature({ queryClient }, node, targetModality) {
180
190
  queryFn: loadPathwayToGeneEdges,
181
191
  });
182
192
 
193
+ if (!node.kgId) {
194
+ const pathwayNodes = await queryClient.fetchQuery({
195
+ queryKey: ['pathwayNodes'],
196
+ staleTime: Infinity,
197
+ queryFn: loadPathwayNodes,
198
+ });
199
+ const foundId = pathwayNodes.find(n => n.term === node.term)?.kgId;
200
+ if (foundId) {
201
+ node.kgId = foundId;
202
+ } else {
203
+ console.warn('Could not find matching pathway node based on term.');
204
+ }
205
+ }
206
+
183
207
  // TODO: support matching using ontology term (rather than requiring kgId)?
184
208
  const matchingEdges = pathwayGeneEdges.filter((/** @type {KgEdge} */ d) => d.source === node.kgId);
185
209
  const matchingGeneIds = matchingEdges.map((/** @type {KgEdge} */ d) => d.target);
package/src/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { BiomarkerSelectSubscriber } from './BiomarkerSelectSubscriber.js';
2
2
  export { ComparativeHeadingSubscriber } from './ComparativeHeadingSubscriber.js';
3
+ export { SampleSetPairManagerSubscriber } from './SampleSetPairManagerSubscriber.js';
3
4
  export {
4
5
  autocompleteFeature,
5
6
  transformFeature,