jbrowse-plugin-msaview 3.2.0 → 3.3.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/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +7 -2
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.d.ts +9 -0
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +20 -0
- package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +7 -5
- package/dist/MsaViewPanel/doLaunchOrthologs.js +64 -30
- package/dist/MsaViewPanel/doLaunchOrthologs.test.js +105 -0
- package/dist/MsaViewPanel/model.d.ts +9 -0
- package/dist/MsaViewPanel/model.js +6 -0
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +30 -30
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/pantherOrthologs.d.ts +79 -0
- package/dist/utils/pantherOrthologs.js +262 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +19 -3
- package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +52 -0
- package/src/MsaViewPanel/doLaunchOrthologs.test.ts +117 -0
- package/src/MsaViewPanel/doLaunchOrthologs.ts +99 -37
- package/src/MsaViewPanel/model.ts +10 -0
- package/src/utils/pantherOrthologs.ts +399 -0
- package/src/version.ts +1 -1
|
@@ -2,10 +2,12 @@ import React, { useMemo, useState } from 'react';
|
|
|
2
2
|
import { Typography } from '@mui/material';
|
|
3
3
|
import { observer } from 'mobx-react';
|
|
4
4
|
import { makeStyles } from 'tss-react/mui';
|
|
5
|
+
import OrthologSourceSelect, { ORTHOLOG_SOURCE_STORAGE_KEY, } from './OrthologSourceSelect';
|
|
5
6
|
import QuerySpeciesSelect from './QuerySpeciesSelect';
|
|
6
7
|
import { orthologLaunchView } from './orthologLaunchView';
|
|
7
8
|
import TextField2 from '../../../components/TextField2';
|
|
8
9
|
import { defaultMaxSpecies } from '../../../utils/ncbiOrthologs';
|
|
10
|
+
import { useLocalStorage } from '../../../utils/useLocalStorage';
|
|
9
11
|
import { getGeneDisplayName, getGeneIdentifiers, getLinearGenomeView, getTranscriptDisplayName, } from '../../util';
|
|
10
12
|
import MsaAlgorithmSelect from '../BlastQuery/MsaAlgorithmSelect';
|
|
11
13
|
import LaunchPanelContent from '../LaunchPanelContent';
|
|
@@ -22,6 +24,7 @@ const OrthologPanel = observer(function ({ handleClose, feature, model, }) {
|
|
|
22
24
|
const view = getLinearGenomeView(model);
|
|
23
25
|
const [launchViewError, setLaunchViewError] = useState();
|
|
24
26
|
const [taxId, setTaxId] = useState(9606);
|
|
27
|
+
const [source, setSource] = useLocalStorage(ORTHOLOG_SOURCE_STORAGE_KEY, 'ncbi');
|
|
25
28
|
const [msaAlgorithm, setMsaAlgorithm] = useState('clustalo');
|
|
26
29
|
const [maxSpecies, setMaxSpecies] = useState(String(defaultMaxSpecies));
|
|
27
30
|
const geneCandidates = useMemo(() => getGeneIdentifiers(feature), [feature]);
|
|
@@ -32,13 +35,14 @@ const OrthologPanel = observer(function ({ handleClose, feature, model, }) {
|
|
|
32
35
|
const rowCountValid = Number.isInteger(rowCount) && rowCount >= 2;
|
|
33
36
|
return (React.createElement(React.Fragment, null,
|
|
34
37
|
React.createElement(LaunchPanelContent, { error: e },
|
|
35
|
-
React.createElement(Typography, { variant: "body2" }, "
|
|
38
|
+
React.createElement(Typography, { variant: "body2" }, "Precomputed orthologs, one gene per species, looked up rather than searched for. No BLAST job to queue."),
|
|
36
39
|
React.createElement("div", null,
|
|
40
|
+
React.createElement(OrthologSourceSelect, { className: classes.selectField, value: source, onChange: setSource }),
|
|
37
41
|
React.createElement(QuerySpeciesSelect, { className: classes.selectField, value: taxId, assemblyName: view.assemblyNames[0], onChange: setTaxId }),
|
|
38
42
|
React.createElement(MsaAlgorithmSelect, { className: classes.selectField, value: msaAlgorithm, onChange: setMsaAlgorithm }),
|
|
39
43
|
React.createElement(TextField2, { variant: "outlined", label: "Rows to align", className: classes.selectField, type: "number", value: maxSpecies, onChange: event => {
|
|
40
44
|
setMaxSpecies(event.target.value);
|
|
41
|
-
}, error: !rowCountValid, helperText:
|
|
45
|
+
}, error: !rowCountValid, helperText: `the closest N species ${source === 'panther' ? 'PANTHER' : 'NCBI'} has` })),
|
|
42
46
|
React.createElement(TranscriptSelector, { feature: feature, ...transcriptSelection })),
|
|
43
47
|
React.createElement(SubmitCancelActions, { model: model, submitDisabled: !proteinSequence || !rowCountValid, onSubmit: () => {
|
|
44
48
|
try {
|
|
@@ -50,6 +54,7 @@ const OrthologPanel = observer(function ({ handleClose, feature, model, }) {
|
|
|
50
54
|
newViewTitle: `Orthologs - ${getGeneDisplayName(feature)} - ${getTranscriptDisplayName(selectedTranscript)}`,
|
|
51
55
|
orthologParams: {
|
|
52
56
|
taxId,
|
|
57
|
+
source,
|
|
53
58
|
maxSpecies: rowCount,
|
|
54
59
|
geneCandidates,
|
|
55
60
|
msaAlgorithm,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { OrthologSource } from '../../../MsaViewPanel/model';
|
|
3
|
+
export declare const ORTHOLOG_SOURCE_STORAGE_KEY = "msaview-ortholog-source";
|
|
4
|
+
export declare const orthologSourceLabels: Record<OrthologSource, string>;
|
|
5
|
+
export default function OrthologSourceSelect({ value, onChange, className, }: {
|
|
6
|
+
value: OrthologSource;
|
|
7
|
+
onChange: (val: OrthologSource) => void;
|
|
8
|
+
className?: string;
|
|
9
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { MenuItem } from '@mui/material';
|
|
3
|
+
import TextField2 from '../../../components/TextField2';
|
|
4
|
+
export const ORTHOLOG_SOURCE_STORAGE_KEY = 'msaview-ortholog-source';
|
|
5
|
+
export const orthologSourceLabels = {
|
|
6
|
+
ncbi: 'NCBI orthologs',
|
|
7
|
+
panther: 'PANTHER',
|
|
8
|
+
};
|
|
9
|
+
// Which species a source can answer for, in the words a reader picking one
|
|
10
|
+
// needs: NCBI's ortholog sets stop at vertebrates and insects, PANTHER's run
|
|
11
|
+
// from human to yeast and Arabidopsis.
|
|
12
|
+
const hints = {
|
|
13
|
+
ncbi: 'vertebrates and insects',
|
|
14
|
+
panther: 'also yeast, worm, fly and plants',
|
|
15
|
+
};
|
|
16
|
+
export default function OrthologSourceSelect({ value, onChange, className, }) {
|
|
17
|
+
return (React.createElement(TextField2, { variant: "outlined", label: "Source", className: className, select: true, value: value, helperText: hints[value], onChange: event => {
|
|
18
|
+
onChange(event.target.value);
|
|
19
|
+
} }, Object.keys(orthologSourceLabels).map(val => (React.createElement(MenuItem, { value: val, key: val }, orthologSourceLabels[val])))));
|
|
20
|
+
}
|
|
@@ -3,12 +3,14 @@ import type { JBrowsePluginMsaViewModel } from './model';
|
|
|
3
3
|
* The no-search-job alternative to doLaunchBlast.
|
|
4
4
|
*
|
|
5
5
|
* BLAST spends 10+ minutes answering "what looks like this sequence" and
|
|
6
|
-
* returns a redundant, accession-labelled hit list. This asks
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* returns a redundant, accession-labelled hit list. This asks the question the
|
|
7
|
+
* alignment actually wants — "what is this gene's ortholog in each species" —
|
|
8
|
+
* which NCBI and PANTHER have already computed, so the lookup returns in
|
|
9
|
+
* seconds and only the EBI alignment (~10s) costs real time. `source` picks
|
|
10
|
+
* which of the two answers: NCBI for vertebrates and insects, PANTHER for
|
|
11
|
+
* everything else (yeast, worm, plants, and a fly gene's vertebrate relatives).
|
|
10
12
|
*
|
|
11
|
-
* The query row is the user's OWN selected transcript, not
|
|
13
|
+
* The query row is the user's OWN selected transcript, not the source's
|
|
12
14
|
* representative protein for the query species, because `connectedFeature`
|
|
13
15
|
* maps genome coordinates through that row — swapping in a different isoform
|
|
14
16
|
* would silently break the genome<->MSA linkage. The query species is therefore
|
|
@@ -1,57 +1,55 @@
|
|
|
1
1
|
import { cleanProteinSequence } from '../LaunchMsaView/util';
|
|
2
2
|
import { launchMSA } from '../utils/msa';
|
|
3
3
|
import { dedupeLabels, fetchOrthologRows, fetchProteinForGene, resolveGeneId, } from '../utils/ncbiOrthologs';
|
|
4
|
+
import { fetchPantherOrthologs } from '../utils/pantherOrthologs';
|
|
4
5
|
import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
|
|
5
6
|
/**
|
|
6
7
|
* The no-search-job alternative to doLaunchBlast.
|
|
7
8
|
*
|
|
8
9
|
* BLAST spends 10+ minutes answering "what looks like this sequence" and
|
|
9
|
-
* returns a redundant, accession-labelled hit list. This asks
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* returns a redundant, accession-labelled hit list. This asks the question the
|
|
11
|
+
* alignment actually wants — "what is this gene's ortholog in each species" —
|
|
12
|
+
* which NCBI and PANTHER have already computed, so the lookup returns in
|
|
13
|
+
* seconds and only the EBI alignment (~10s) costs real time. `source` picks
|
|
14
|
+
* which of the two answers: NCBI for vertebrates and insects, PANTHER for
|
|
15
|
+
* everything else (yeast, worm, plants, and a fly gene's vertebrate relatives).
|
|
13
16
|
*
|
|
14
|
-
* The query row is the user's OWN selected transcript, not
|
|
17
|
+
* The query row is the user's OWN selected transcript, not the source's
|
|
15
18
|
* representative protein for the query species, because `connectedFeature`
|
|
16
19
|
* maps genome coordinates through that row — swapping in a different isoform
|
|
17
20
|
* would silently break the genome<->MSA linkage. The query species is therefore
|
|
18
21
|
* excluded from the ortholog set rather than appearing twice.
|
|
19
22
|
*/
|
|
20
23
|
export async function doLaunchOrthologs({ self, }) {
|
|
21
|
-
const { taxId, taxa, maxSpecies, geneCandidates, msaAlgorithm, proteinSequence, } = self.orthologParams;
|
|
24
|
+
const { taxId, taxa, maxSpecies, geneCandidates, msaAlgorithm, proteinSequence, source = 'ncbi', } = self.orthologParams;
|
|
22
25
|
const onProgress = (arg) => {
|
|
23
26
|
self.setProgress(arg);
|
|
24
27
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
const request = {
|
|
29
|
+
taxId,
|
|
30
|
+
geneCandidates,
|
|
31
|
+
taxa: taxa ? new Set(taxa) : undefined,
|
|
32
|
+
// the query species is represented by the query row below
|
|
33
|
+
exclude: taxId,
|
|
34
|
+
limit: maxSpecies,
|
|
35
|
+
onProgress,
|
|
36
|
+
};
|
|
37
|
+
const { geneId, representative, rows } = source === 'panther'
|
|
38
|
+
? await findPantherOrthologs(request)
|
|
39
|
+
: await findNcbiOrthologs(request);
|
|
30
40
|
// The query row. The dialog always supplies it — it is the user's OWN
|
|
31
41
|
// selected transcript, which is what makes `connectedFeature` map genome
|
|
32
42
|
// coordinates through this row. A launch that has no transcript to translate
|
|
33
|
-
// (a session spec naming only a gene) falls back to
|
|
34
|
-
// protein for the resolved gene, which is the same choice
|
|
35
|
-
// other row, so the alignment is the one
|
|
36
|
-
|
|
43
|
+
// (a session spec naming only a gene) falls back to the source's
|
|
44
|
+
// representative protein for the resolved gene, which is the same choice
|
|
45
|
+
// made for every other row, so the alignment is the one the source would
|
|
46
|
+
// build for that gene.
|
|
37
47
|
const cleanedSeq = proteinSequence
|
|
38
48
|
? cleanProteinSequence(proteinSequence)
|
|
39
49
|
: representative?.sequence;
|
|
40
50
|
if (!cleanedSeq) {
|
|
41
|
-
throw new Error(`No query protein: none was supplied and NCBI returned no representative protein for gene ${
|
|
51
|
+
throw new Error(`No query protein: none was supplied and ${source === 'panther' ? 'PANTHER' : 'NCBI'} returned no representative protein for gene ${geneId}.`);
|
|
42
52
|
}
|
|
43
|
-
// Every species NCBI has an ortholog for, when a launch names none, capped at
|
|
44
|
-
// maxSpecies. A launch that wants specific species lists them; one that just
|
|
45
|
-
// wants "this gene across species" gets NCBI's own order, which leads with the
|
|
46
|
-
// reference organisms.
|
|
47
|
-
const rows = await fetchOrthologRows({
|
|
48
|
-
geneId: resolved.geneId,
|
|
49
|
-
taxa: taxa ? new Set(taxa) : undefined,
|
|
50
|
-
// the query species is represented by the query row above
|
|
51
|
-
exclude: taxId,
|
|
52
|
-
limit: maxSpecies,
|
|
53
|
-
onProgress,
|
|
54
|
-
});
|
|
55
53
|
// The query row is named for its species like every other row, with a suffix
|
|
56
54
|
// marking it as the one the genome view is linked to. A bare `QUERY` among
|
|
57
55
|
// ninety-nine named species reads as a row whose species failed to resolve,
|
|
@@ -66,7 +64,7 @@ export async function doLaunchOrthologs({ self, }) {
|
|
|
66
64
|
const queryLabel = await queryRowLabel(taxId, rows);
|
|
67
65
|
self.setQuerySeqName(queryLabel);
|
|
68
66
|
const treeMetadata = {
|
|
69
|
-
[queryLabel]: buildQueryMetadata(self,
|
|
67
|
+
[queryLabel]: buildQueryMetadata(self, geneId, cleanedSeq, representative),
|
|
70
68
|
};
|
|
71
69
|
for (const row of rows) {
|
|
72
70
|
treeMetadata[row.label] = buildRowMetadata(row);
|
|
@@ -84,6 +82,42 @@ export async function doLaunchOrthologs({ self, }) {
|
|
|
84
82
|
treeMetadata: JSON.stringify(treeMetadata),
|
|
85
83
|
};
|
|
86
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Every species NCBI has an ortholog for, when a launch names none, capped at
|
|
87
|
+
* `limit`. A launch that wants specific species lists them; one that just
|
|
88
|
+
* wants "this gene across species" gets NCBI's own order, which leads with the
|
|
89
|
+
* reference organisms.
|
|
90
|
+
*/
|
|
91
|
+
async function findNcbiOrthologs({ taxId, geneCandidates, onProgress, ...rest }) {
|
|
92
|
+
onProgress('Resolving gene at NCBI...');
|
|
93
|
+
const resolved = await resolveGeneId(geneCandidates, taxId);
|
|
94
|
+
if (!resolved) {
|
|
95
|
+
throw new Error(`Could not resolve any of ${geneCandidates.join(', ')} to an NCBI gene in taxon ${taxId}. Try the NCBI BLAST tab, which needs no gene identifier.`);
|
|
96
|
+
}
|
|
97
|
+
const representative = await fetchRepresentativeQueryProtein(resolved.geneId);
|
|
98
|
+
const rows = await fetchOrthologRows({
|
|
99
|
+
geneId: resolved.geneId,
|
|
100
|
+
onProgress,
|
|
101
|
+
...rest,
|
|
102
|
+
});
|
|
103
|
+
return { geneId: resolved.geneId, representative, rows };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* One `matchortho` call resolves the gene, names its own UniProt entry and
|
|
107
|
+
* lists an ortholog per genome, so the representative protein needs no second
|
|
108
|
+
* lookup here.
|
|
109
|
+
*/
|
|
110
|
+
async function findPantherOrthologs({ geneCandidates, ...rest }) {
|
|
111
|
+
const found = await fetchPantherOrthologs({
|
|
112
|
+
candidates: geneCandidates,
|
|
113
|
+
...rest,
|
|
114
|
+
});
|
|
115
|
+
return {
|
|
116
|
+
geneId: found.query?.geneRef ?? found.matched,
|
|
117
|
+
representative: found.query,
|
|
118
|
+
rows: found.rows,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
87
121
|
/**
|
|
88
122
|
* `<species>_query`, unique against the ortholog labels. Falls back to the bare
|
|
89
123
|
* marker when NCBI cannot name the taxon, which is a naming failure and must not
|
|
@@ -120,7 +154,7 @@ async function fetchRepresentativeQueryProtein(geneId) {
|
|
|
120
154
|
/**
|
|
121
155
|
* The query row carries an Accession — which is what drives the automatic CDD
|
|
122
156
|
* overlay (afterCreateAutoruns.autoLoadProteinDomains -> loadProteinDomains) —
|
|
123
|
-
* ONLY when its sequence is byte-identical to the
|
|
157
|
+
* ONLY when its sequence is byte-identical to the protein that accession
|
|
124
158
|
* names. Attaching it unconditionally would put every domain box at an offset
|
|
125
159
|
* whenever the user picked a non-representative isoform, which is a silently
|
|
126
160
|
* wrong figure rather than a missing one. A launch that took the representative
|
|
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
|
2
2
|
import { doLaunchOrthologs } from './doLaunchOrthologs';
|
|
3
3
|
import { launchMSA } from '../utils/msa';
|
|
4
4
|
import { defaultMaxSpecies, fetchOrthologRows, fetchProteinForGene, resolveGeneId, } from '../utils/ncbiOrthologs';
|
|
5
|
+
import { fetchPantherOrthologs } from '../utils/pantherOrthologs';
|
|
5
6
|
import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
|
|
6
7
|
// Every network call is mocked and nothing else is. What is under test is the
|
|
7
8
|
// argument shaping either side of those calls -- which species get asked for,
|
|
@@ -14,11 +15,15 @@ vi.mock('../utils/ncbiOrthologs', async (importOriginal) => ({
|
|
|
14
15
|
fetchProteinForGene: vi.fn(),
|
|
15
16
|
fetchOrthologRows: vi.fn(),
|
|
16
17
|
}));
|
|
18
|
+
vi.mock('../utils/pantherOrthologs', () => ({
|
|
19
|
+
fetchPantherOrthologs: vi.fn(),
|
|
20
|
+
}));
|
|
17
21
|
vi.mock('../utils/msa', () => ({ launchMSA: vi.fn() }));
|
|
18
22
|
vi.mock('../utils/taxonomyNames', () => ({ fetchTaxonomyInfo: vi.fn() }));
|
|
19
23
|
const mockResolveGeneId = vi.mocked(resolveGeneId);
|
|
20
24
|
const mockFetchProtein = vi.mocked(fetchProteinForGene);
|
|
21
25
|
const mockFetchRows = vi.mocked(fetchOrthologRows);
|
|
26
|
+
const mockFetchPanther = vi.mocked(fetchPantherOrthologs);
|
|
22
27
|
const mockLaunchMSA = vi.mocked(launchMSA);
|
|
23
28
|
const mockFetchTaxonomy = vi.mocked(fetchTaxonomyInfo);
|
|
24
29
|
const HUMAN = 9606;
|
|
@@ -199,3 +204,103 @@ describe('the Accession that drives the domain overlay', () => {
|
|
|
199
204
|
expect(queryMetadata(result).Accession).toBeUndefined();
|
|
200
205
|
});
|
|
201
206
|
});
|
|
207
|
+
// The second source. What is under test is the dispatch and what the PANTHER
|
|
208
|
+
// result becomes on the query row -- the rows themselves are shaped upstream,
|
|
209
|
+
// and the tail of the launch (labels, aligner, metadata) is the same code the
|
|
210
|
+
// NCBI tests above already cover.
|
|
211
|
+
describe('the PANTHER source', () => {
|
|
212
|
+
const YEAST = 559292;
|
|
213
|
+
const found = {
|
|
214
|
+
matched: 'CDC28',
|
|
215
|
+
query: {
|
|
216
|
+
code: 'YEAST',
|
|
217
|
+
accession: 'P00546',
|
|
218
|
+
geneRef: 'SGD=S000000364',
|
|
219
|
+
sequence: 'MSGELANYKRLEKVGEGTYGVVYKA',
|
|
220
|
+
},
|
|
221
|
+
rows: [
|
|
222
|
+
{
|
|
223
|
+
taxId: HUMAN,
|
|
224
|
+
label: 'human',
|
|
225
|
+
scientificName: 'Homo sapiens',
|
|
226
|
+
commonName: 'human',
|
|
227
|
+
geneId: 'HGNC=1771',
|
|
228
|
+
protein: 'P24941',
|
|
229
|
+
sequence: 'MENFQKVEKIGEGTYGVVYKARNK',
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
};
|
|
233
|
+
beforeEach(() => {
|
|
234
|
+
mockFetchPanther.mockResolvedValue(found);
|
|
235
|
+
mockFetchTaxonomy.mockResolvedValue(new Map([[YEAST, { sciname: 'Saccharomyces cerevisiae' }]]));
|
|
236
|
+
});
|
|
237
|
+
test('source omitted is NCBI, so an old launch never reaches PANTHER', async () => {
|
|
238
|
+
await doLaunchOrthologs({ self: makeModel(params()) });
|
|
239
|
+
expect(mockFetchPanther).not.toHaveBeenCalled();
|
|
240
|
+
expect(mockResolveGeneId).toHaveBeenCalled();
|
|
241
|
+
});
|
|
242
|
+
test('source panther asks PANTHER with the same species semantics, and skips NCBI', async () => {
|
|
243
|
+
await doLaunchOrthologs({
|
|
244
|
+
self: makeModel({
|
|
245
|
+
taxId: YEAST,
|
|
246
|
+
source: 'panther',
|
|
247
|
+
geneCandidates: ['CDC28'],
|
|
248
|
+
msaAlgorithm: 'clustalo',
|
|
249
|
+
taxa: [HUMAN, YEAST],
|
|
250
|
+
maxSpecies: 7,
|
|
251
|
+
}),
|
|
252
|
+
});
|
|
253
|
+
expect(mockResolveGeneId).not.toHaveBeenCalled();
|
|
254
|
+
expect(mockFetchRows).not.toHaveBeenCalled();
|
|
255
|
+
const { candidates, taxId, taxa, exclude, limit } = mockFetchPanther.mock.calls[0][0];
|
|
256
|
+
expect(candidates).toEqual(['CDC28']);
|
|
257
|
+
expect(taxId).toBe(YEAST);
|
|
258
|
+
expect([...taxa]).toEqual([HUMAN, YEAST]);
|
|
259
|
+
expect(exclude).toBe(YEAST);
|
|
260
|
+
expect(limit).toBe(7);
|
|
261
|
+
});
|
|
262
|
+
test("the query row is PANTHER's own entry for the gene when no sequence was supplied, and carries its UniProt accession for the domain overlay", async () => {
|
|
263
|
+
const result = await doLaunchOrthologs({
|
|
264
|
+
self: makeModel({
|
|
265
|
+
taxId: YEAST,
|
|
266
|
+
source: 'panther',
|
|
267
|
+
geneCandidates: ['CDC28'],
|
|
268
|
+
msaAlgorithm: 'clustalo',
|
|
269
|
+
}),
|
|
270
|
+
});
|
|
271
|
+
expect(queryRowName()).toBe('Saccharomyces_cerevisiae_query');
|
|
272
|
+
expect(queryRowSent()).toBe(found.query.sequence);
|
|
273
|
+
expect(queryMetadata(result)).toEqual({
|
|
274
|
+
'Gene ID': 'SGD=S000000364',
|
|
275
|
+
Accession: 'P00546',
|
|
276
|
+
});
|
|
277
|
+
expect(JSON.parse(result.treeMetadata).human).toMatchObject({
|
|
278
|
+
Accession: 'P24941',
|
|
279
|
+
'Gene ID': 'HGNC=1771',
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
test('a supplied sequence still wins, and a different isoform earns no Accession', async () => {
|
|
283
|
+
const result = await doLaunchOrthologs({
|
|
284
|
+
self: makeModel({
|
|
285
|
+
taxId: YEAST,
|
|
286
|
+
source: 'panther',
|
|
287
|
+
geneCandidates: ['CDC28'],
|
|
288
|
+
msaAlgorithm: 'clustalo',
|
|
289
|
+
proteinSequence: 'MDIFFERENTISOFORM',
|
|
290
|
+
}),
|
|
291
|
+
});
|
|
292
|
+
expect(queryRowSent()).toBe('MDIFFERENTISOFORM');
|
|
293
|
+
expect(queryMetadata(result).Accession).toBeUndefined();
|
|
294
|
+
});
|
|
295
|
+
test('names PANTHER when it has no protein for the query row', async () => {
|
|
296
|
+
mockFetchPanther.mockResolvedValue({ ...found, query: undefined });
|
|
297
|
+
await expect(doLaunchOrthologs({
|
|
298
|
+
self: makeModel({
|
|
299
|
+
taxId: YEAST,
|
|
300
|
+
source: 'panther',
|
|
301
|
+
geneCandidates: ['CDC28'],
|
|
302
|
+
msaAlgorithm: 'clustalo',
|
|
303
|
+
}),
|
|
304
|
+
})).rejects.toThrow(/PANTHER returned no representative protein/);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
@@ -17,9 +17,18 @@ export interface BlastParams {
|
|
|
17
17
|
selectedTranscript?: Feature;
|
|
18
18
|
proteinSequence: string;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Where the ortholog set comes from. NCBI's sets cover vertebrates and
|
|
22
|
+
* insects; PANTHER's span its 144 reference proteomes, human to yeast to
|
|
23
|
+
* Arabidopsis, so a gene from outside NCBI's scope aligns only through it.
|
|
24
|
+
*/
|
|
25
|
+
export declare const orthologSources: readonly ["ncbi", "panther"];
|
|
26
|
+
export type OrthologSource = (typeof orthologSources)[number];
|
|
20
27
|
export interface OrthologParams {
|
|
21
28
|
/** NCBI taxon id of the assembly the query gene came from */
|
|
22
29
|
taxId: number;
|
|
30
|
+
/** `ncbi` when omitted, so every launch written before this key keeps its meaning */
|
|
31
|
+
source?: OrthologSource;
|
|
23
32
|
/**
|
|
24
33
|
* taxon ids to include as rows. The query taxon has its own row already, so
|
|
25
34
|
* it is excluded from this set whether or not it is named.
|
|
@@ -6,6 +6,12 @@ import { autorun } from 'mobx';
|
|
|
6
6
|
import { MSAModelF } from 'react-msaview';
|
|
7
7
|
import { autoLoadProteinDomains, launchBlastIfNeeded, launchOrthologsIfNeeded, loadStoredData, observeProteinHighlights, processInit, runCleanup, storeDataToIndexedDB, syncGenomeHoverToMsaColumn, } from './afterCreateAutoruns';
|
|
8
8
|
import { msaCoordToGenomeCoord, msaCoordToGenomeRegions, } from './msaCoordToGenomeCoord';
|
|
9
|
+
/**
|
|
10
|
+
* Where the ortholog set comes from. NCBI's sets cover vertebrates and
|
|
11
|
+
* insects; PANTHER's span its 144 reference proteomes, human to yeast to
|
|
12
|
+
* Arabidopsis, so a gene from outside NCBI's scope aligns only through it.
|
|
13
|
+
*/
|
|
14
|
+
export const orthologSources = ['ncbi', 'panther'];
|
|
9
15
|
/**
|
|
10
16
|
* #stateModel MsaViewPlugin
|
|
11
17
|
* extends
|