jbrowse-plugin-msaview 3.3.0 → 3.4.1
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/AddHighlightModel/GenomeMouseoverHighlight.js +1 -1
- package/dist/AddHighlightModel/MsaToGenomeHighlight.js +1 -1
- package/dist/AddHighlightModel/index.js +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +60 -18
- package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/BlastPanel.js +2 -2
- package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.d.ts +12 -0
- package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.js +21 -2
- package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.d.ts +1 -0
- package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.js +29 -0
- package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +28 -0
- package/dist/LaunchMsaView/components/BlastQuery/consts.js +21 -0
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.d.ts +7 -0
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.js +42 -0
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.d.ts +1 -0
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.js +32 -0
- package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +1 -1
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +5 -4
- package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +1 -1
- package/dist/LaunchMsaView/components/useFeatureSequence.js +1 -1
- package/dist/LaunchMsaView/detectQueryRow.d.ts +15 -2
- package/dist/LaunchMsaView/detectQueryRow.js +20 -21
- package/dist/LaunchMsaView/detectQueryRow.test.js +15 -15
- package/dist/LaunchMsaView/useQueryRowName.js +5 -8
- package/dist/LaunchMsaViewExtensionPoint/index.js +11 -6
- package/dist/LaunchMsaViewExtensionPoint/index.test.d.ts +1 -0
- package/dist/LaunchMsaViewExtensionPoint/index.test.js +43 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +15 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.js +95 -72
- package/dist/MsaViewPanel/components/ErrorBoundary.d.ts +2 -2
- package/dist/MsaViewPanel/components/JobLink.js +7 -1
- package/dist/MsaViewPanel/components/LaunchProgress.d.ts +17 -0
- package/dist/MsaViewPanel/components/LaunchProgress.js +50 -0
- package/dist/MsaViewPanel/components/MsaViewPanel.js +6 -3
- package/dist/MsaViewPanel/components/MsaViewPanel.test.d.ts +1 -0
- package/dist/MsaViewPanel/components/MsaViewPanel.test.js +87 -0
- package/dist/MsaViewPanel/doLaunchBlast.d.ts +4 -2
- package/dist/MsaViewPanel/doLaunchBlast.js +86 -56
- package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +3 -1
- package/dist/MsaViewPanel/doLaunchOrthologs.js +6 -5
- package/dist/MsaViewPanel/doLaunchOrthologs.test.js +39 -25
- package/dist/MsaViewPanel/genomeToMSA.js +4 -2
- package/dist/MsaViewPanel/genomeToMSA.test.js +34 -0
- package/dist/MsaViewPanel/model.d.ts +190 -19
- package/dist/MsaViewPanel/model.js +51 -1
- package/dist/MsaViewPanel/msaDataStore.d.ts +5 -3
- package/dist/MsaViewPanel/msaDataStore.js +16 -7
- package/dist/MsaViewPanel/msaDataStore.test.d.ts +1 -0
- package/dist/MsaViewPanel/msaDataStore.test.js +44 -0
- package/dist/MsaViewPanel/observeProteinHighlights.test.js +11 -0
- package/dist/MsaViewPanel/runLaunch.d.ts +38 -0
- package/dist/MsaViewPanel/runLaunch.js +65 -0
- package/dist/MsaViewPanel/runLaunch.test.d.ts +1 -0
- package/dist/MsaViewPanel/runLaunch.test.js +129 -0
- package/dist/MsaViewPanel/storedData.test.d.ts +1 -0
- package/dist/MsaViewPanel/storedData.test.js +160 -0
- package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +1 -0
- package/dist/MsaViewPanel/util.d.ts +18 -0
- package/dist/MsaViewPanel/util.js +17 -0
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +47 -35
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/blastCache.d.ts +10 -6
- package/dist/utils/blastCache.js +47 -3
- package/dist/utils/blastCache.test.d.ts +1 -0
- package/dist/utils/blastCache.test.js +72 -0
- package/dist/utils/ebiBlast.d.ts +5 -3
- package/dist/utils/ebiBlast.js +6 -4
- package/dist/utils/ebiJobDispatcher.d.ts +6 -3
- package/dist/utils/ebiJobDispatcher.js +13 -6
- package/dist/utils/fetch.d.ts +8 -1
- package/dist/utils/fetch.js +28 -2
- package/dist/utils/msa.d.ts +15 -1
- package/dist/utils/msa.js +40 -13
- package/dist/utils/msaRows.d.ts +31 -0
- package/dist/utils/msaRows.js +67 -0
- package/dist/utils/ncbiDomains.d.ts +2 -2
- package/dist/utils/ncbiOrthologs.d.ts +11 -1
- package/dist/utils/ncbiOrthologs.js +26 -7
- package/dist/utils/ncbiOrthologs.test.js +23 -1
- package/dist/utils/pantherOrthologs.js +2 -10
- package/dist/utils/phmmer.d.ts +54 -0
- package/dist/utils/phmmer.js +120 -0
- package/dist/utils/poll.d.ts +6 -1
- package/dist/utils/poll.js +7 -2
- package/dist/utils/taxonomyNames.d.ts +1 -1
- package/dist/utils/taxonomyNames.js +6 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +30 -24
- package/src/AddHighlightModel/GenomeMouseoverHighlight.tsx +1 -1
- package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +1 -1
- package/src/AddHighlightModel/index.tsx +1 -1
- package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +85 -31
- package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -1
- package/src/LaunchMsaView/components/BlastQuery/BlastPanel.tsx +4 -4
- package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.ts +50 -0
- package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.tsx +23 -3
- package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +1 -1
- package/src/LaunchMsaView/components/BlastQuery/consts.ts +40 -0
- package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.ts +43 -0
- package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.ts +64 -0
- package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -1
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +7 -7
- package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +1 -1
- package/src/LaunchMsaView/components/useFeatureSequence.ts +1 -1
- package/src/LaunchMsaView/detectQueryRow.test.ts +17 -15
- package/src/LaunchMsaView/detectQueryRow.ts +34 -23
- package/src/LaunchMsaView/useQueryRowName.ts +6 -9
- package/src/LaunchMsaViewExtensionPoint/index.test.ts +51 -0
- package/src/LaunchMsaViewExtensionPoint/index.ts +21 -6
- package/src/MsaViewPanel/afterCreateAutoruns.ts +102 -68
- package/src/MsaViewPanel/components/ErrorBoundary.tsx +2 -1
- package/src/MsaViewPanel/components/JobLink.tsx +7 -2
- package/src/MsaViewPanel/components/LaunchProgress.tsx +80 -0
- package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +104 -0
- package/src/MsaViewPanel/components/MsaViewPanel.tsx +7 -4
- package/src/MsaViewPanel/doLaunchBlast.ts +134 -72
- package/src/MsaViewPanel/doLaunchOrthologs.test.ts +43 -28
- package/src/MsaViewPanel/doLaunchOrthologs.ts +9 -5
- package/src/MsaViewPanel/genomeToMSA.test.ts +37 -0
- package/src/MsaViewPanel/genomeToMSA.ts +6 -2
- package/src/MsaViewPanel/model.ts +82 -6
- package/src/MsaViewPanel/msaDataStore.test.ts +54 -0
- package/src/MsaViewPanel/msaDataStore.ts +22 -13
- package/src/MsaViewPanel/observeProteinHighlights.test.ts +13 -0
- package/src/MsaViewPanel/runLaunch.test.ts +154 -0
- package/src/MsaViewPanel/runLaunch.ts +102 -0
- package/src/MsaViewPanel/storedData.test.ts +196 -0
- package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +1 -0
- package/src/MsaViewPanel/util.ts +18 -0
- package/src/utils/blastCache.test.ts +86 -0
- package/src/utils/blastCache.ts +67 -13
- package/src/utils/ebiBlast.ts +9 -3
- package/src/utils/ebiJobDispatcher.ts +18 -3
- package/src/utils/fetch.ts +29 -2
- package/src/utils/msa.ts +51 -12
- package/src/utils/msaRows.ts +95 -0
- package/src/utils/ncbiDomains.ts +4 -2
- package/src/utils/ncbiOrthologs.test.ts +25 -0
- package/src/utils/ncbiOrthologs.ts +27 -7
- package/src/utils/pantherOrthologs.ts +6 -11
- package/src/utils/phmmer.ts +178 -0
- package/src/utils/poll.ts +8 -1
- package/src/utils/taxonomyNames.ts +6 -1
- package/src/version.ts +1 -1
- package/dist/MsaViewPanel/components/LoadingBLAST.d.ts +0 -6
- package/dist/MsaViewPanel/components/LoadingBLAST.js +0 -26
- package/src/MsaViewPanel/components/LoadingBLAST.tsx +0 -48
|
@@ -2,29 +2,68 @@ import { makeId, strip } from '../LaunchMsaView/components/util';
|
|
|
2
2
|
import { cleanProteinSequence } from '../LaunchMsaView/util';
|
|
3
3
|
import { saveBlastResult } from '../utils/blastCache';
|
|
4
4
|
import { queryEbiBlast } from '../utils/ebiBlast';
|
|
5
|
-
import { launchMSA } from '../utils/msa';
|
|
5
|
+
import { launchMSA, launchTree } from '../utils/msa';
|
|
6
|
+
import { buildPhmmerMsa, buildRowMetadata } from '../utils/msaRows';
|
|
7
|
+
import { queryPhmmer } from '../utils/phmmer';
|
|
6
8
|
import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
|
|
7
|
-
export async function doLaunchBlast({ self, }) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
export async function doLaunchBlast({ self, scope, }) {
|
|
10
|
+
// kept whole rather than destructured: the database's type depends on
|
|
11
|
+
// searchProgram, and pulling the two apart loses the link between them
|
|
12
|
+
const params = self.blastParams;
|
|
13
|
+
const { selectedTranscript } = params;
|
|
14
|
+
const cleanedSeq = cleanProteinSequence(params.proteinSequence);
|
|
15
|
+
const { onProgress, onRid, signal } = scope;
|
|
16
|
+
const { msa, tree, treeMetadata, rid } = params.searchProgram === 'phmmer'
|
|
17
|
+
? await runPhmmer({
|
|
18
|
+
query: cleanedSeq,
|
|
19
|
+
database: params.blastDatabase,
|
|
20
|
+
onProgress,
|
|
21
|
+
onRid,
|
|
22
|
+
signal,
|
|
23
|
+
})
|
|
24
|
+
: await runBlast({
|
|
25
|
+
query: cleanedSeq,
|
|
26
|
+
blastDatabase: params.blastDatabase,
|
|
27
|
+
msaAlgorithm: params.msaAlgorithm,
|
|
28
|
+
onProgress,
|
|
29
|
+
onRid,
|
|
30
|
+
signal,
|
|
31
|
+
});
|
|
32
|
+
const treeMetadataJson = JSON.stringify(treeMetadata);
|
|
33
|
+
await saveBlastResult({
|
|
34
|
+
proteinSequence: cleanedSeq,
|
|
35
|
+
blastDatabase: params.blastDatabase,
|
|
36
|
+
msaAlgorithm: params.msaAlgorithm,
|
|
37
|
+
searchProgram: params.searchProgram,
|
|
38
|
+
msa,
|
|
39
|
+
tree,
|
|
40
|
+
treeMetadata: treeMetadataJson,
|
|
41
|
+
rid,
|
|
42
|
+
geneId: selectedTranscript?.get('parentId'),
|
|
43
|
+
transcriptId: selectedTranscript?.id(),
|
|
44
|
+
transcriptName: selectedTranscript?.get('name') ?? selectedTranscript?.get('id'),
|
|
45
|
+
geneName: selectedTranscript?.get('gene_name') ??
|
|
46
|
+
selectedTranscript?.get('parentId'),
|
|
47
|
+
});
|
|
48
|
+
return { msa, tree, treeMetadata: treeMetadataJson };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* BLAST returns each hit already aligned to the query, but pairwise and one hit
|
|
52
|
+
* at a time, so the alignments are stripped back off and every hit is realigned
|
|
53
|
+
* together by a dedicated aligner.
|
|
54
|
+
*/
|
|
55
|
+
async function runBlast({ query, blastDatabase, msaAlgorithm, onProgress, onRid, signal, }) {
|
|
13
56
|
const { hits, rid } = await queryEbiBlast({
|
|
14
|
-
query
|
|
57
|
+
query,
|
|
15
58
|
blastDatabase,
|
|
16
59
|
onProgress,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
onRid: r => {
|
|
20
|
-
self.setRid(r);
|
|
21
|
-
},
|
|
60
|
+
onRid,
|
|
61
|
+
signal,
|
|
22
62
|
});
|
|
23
|
-
|
|
24
|
-
const
|
|
63
|
+
onProgress('Fetching species taxonomy info...');
|
|
64
|
+
const taxonomyInfo = await fetchTaxonomyInfo(hits
|
|
25
65
|
.map(h => h.description[0]?.taxid)
|
|
26
|
-
.filter((t) => t !== undefined);
|
|
27
|
-
const taxonomyInfo = await fetchTaxonomyInfo(taxids);
|
|
66
|
+
.filter((t) => t !== undefined));
|
|
28
67
|
const treeMetadata = {};
|
|
29
68
|
const sequences = hits.map(h => {
|
|
30
69
|
const desc = h.description[0] ?? {
|
|
@@ -33,52 +72,43 @@ export async function doLaunchBlast({ self, }) {
|
|
|
33
72
|
sciname: 'unknown',
|
|
34
73
|
};
|
|
35
74
|
const rowName = makeId(desc, taxonomyInfo);
|
|
36
|
-
const seq = strip(h.hsps[0]?.hseq ?? '');
|
|
37
75
|
treeMetadata[rowName] = buildRowMetadata(desc, taxonomyInfo);
|
|
38
|
-
return `>${rowName}\n${
|
|
76
|
+
return `>${rowName}\n${strip(h.hsps[0]?.hseq ?? '')}`;
|
|
39
77
|
});
|
|
40
78
|
const result = await launchMSA({
|
|
41
79
|
algorithm: msaAlgorithm,
|
|
42
|
-
sequence: [`>QUERY\n${
|
|
80
|
+
sequence: [`>QUERY\n${query}`, ...sequences].join('\n'),
|
|
43
81
|
onProgress,
|
|
82
|
+
signal,
|
|
44
83
|
});
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
84
|
+
return { ...result, treeMetadata, rid };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* phmmer aligns every hit to a profile of the query as it searches, so its own
|
|
88
|
+
* output is the MSA and there is no realignment step — the hits keep the
|
|
89
|
+
* placement HMMER gave them, and the query row is derived from the alignment's
|
|
90
|
+
* match columns rather than being aligned back in afterwards. That leaves no
|
|
91
|
+
* aligner run to take a tree from, so the tree is built from this alignment.
|
|
92
|
+
*/
|
|
93
|
+
async function runPhmmer({ query, database, onProgress, onRid, signal, }) {
|
|
94
|
+
const { rows, queryRow, rid } = await queryPhmmer({
|
|
95
|
+
query,
|
|
96
|
+
database,
|
|
97
|
+
onProgress,
|
|
98
|
+
onRid,
|
|
99
|
+
signal,
|
|
100
|
+
});
|
|
101
|
+
onProgress('Fetching species taxonomy info...');
|
|
102
|
+
const taxonomyInfo = await fetchTaxonomyInfo(rows.map(r => r.taxid).filter((t) => t !== undefined));
|
|
103
|
+
const { msa, treeMetadata } = buildPhmmerMsa({
|
|
104
|
+
rows,
|
|
105
|
+
queryRow,
|
|
106
|
+
taxonomyInfo,
|
|
59
107
|
});
|
|
60
108
|
return {
|
|
61
|
-
|
|
62
|
-
|
|
109
|
+
msa,
|
|
110
|
+
tree: await launchTree({ alignment: msa, onProgress, signal }),
|
|
111
|
+
treeMetadata,
|
|
112
|
+
rid,
|
|
63
113
|
};
|
|
64
114
|
}
|
|
65
|
-
function buildRowMetadata(desc, taxonomyInfo) {
|
|
66
|
-
const metadata = {};
|
|
67
|
-
const taxInfo = desc.taxid ? taxonomyInfo.get(desc.taxid) : undefined;
|
|
68
|
-
if (taxInfo?.sciname) {
|
|
69
|
-
metadata['Scientific name'] = taxInfo.sciname;
|
|
70
|
-
}
|
|
71
|
-
if (taxInfo?.commonName) {
|
|
72
|
-
metadata['Common name'] = taxInfo.commonName;
|
|
73
|
-
}
|
|
74
|
-
if (desc.accession) {
|
|
75
|
-
metadata.Accession = desc.accession;
|
|
76
|
-
}
|
|
77
|
-
if (desc.id) {
|
|
78
|
-
metadata.ID = desc.id;
|
|
79
|
-
}
|
|
80
|
-
if (desc.title) {
|
|
81
|
-
metadata.Description = desc.title;
|
|
82
|
-
}
|
|
83
|
-
return metadata;
|
|
84
|
-
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { JBrowsePluginMsaViewModel } from './model';
|
|
2
|
+
import type { LaunchScope } from './runLaunch';
|
|
2
3
|
/**
|
|
3
4
|
* The no-search-job alternative to doLaunchBlast.
|
|
4
5
|
*
|
|
@@ -16,8 +17,9 @@ import type { JBrowsePluginMsaViewModel } from './model';
|
|
|
16
17
|
* would silently break the genome<->MSA linkage. The query species is therefore
|
|
17
18
|
* excluded from the ortholog set rather than appearing twice.
|
|
18
19
|
*/
|
|
19
|
-
export declare function doLaunchOrthologs({ self, }: {
|
|
20
|
+
export declare function doLaunchOrthologs({ self, scope, }: {
|
|
20
21
|
self: JBrowsePluginMsaViewModel;
|
|
22
|
+
scope: LaunchScope;
|
|
21
23
|
}): Promise<{
|
|
22
24
|
treeMetadata: string;
|
|
23
25
|
msa: string;
|
|
@@ -20,11 +20,9 @@ import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
|
|
|
20
20
|
* would silently break the genome<->MSA linkage. The query species is therefore
|
|
21
21
|
* excluded from the ortholog set rather than appearing twice.
|
|
22
22
|
*/
|
|
23
|
-
export async function doLaunchOrthologs({ self, }) {
|
|
23
|
+
export async function doLaunchOrthologs({ self, scope, }) {
|
|
24
24
|
const { taxId, taxa, maxSpecies, geneCandidates, msaAlgorithm, proteinSequence, source = 'ncbi', } = self.orthologParams;
|
|
25
|
-
const onProgress
|
|
26
|
-
self.setProgress(arg);
|
|
27
|
-
};
|
|
25
|
+
const { onProgress, act, signal } = scope;
|
|
28
26
|
const request = {
|
|
29
27
|
taxId,
|
|
30
28
|
geneCandidates,
|
|
@@ -62,7 +60,9 @@ export async function doLaunchOrthologs({ self, }) {
|
|
|
62
60
|
// token, and a collision would silently point the coordinate mapping at
|
|
63
61
|
// another animal's row.
|
|
64
62
|
const queryLabel = await queryRowLabel(taxId, rows);
|
|
65
|
-
|
|
63
|
+
act(() => {
|
|
64
|
+
self.setQuerySeqName(queryLabel);
|
|
65
|
+
});
|
|
66
66
|
const treeMetadata = {
|
|
67
67
|
[queryLabel]: buildQueryMetadata(self, geneId, cleanedSeq, representative),
|
|
68
68
|
};
|
|
@@ -76,6 +76,7 @@ export async function doLaunchOrthologs({ self, }) {
|
|
|
76
76
|
...rows.map(r => `>${r.label}\n${r.sequence}`),
|
|
77
77
|
].join('\n'),
|
|
78
78
|
onProgress,
|
|
79
|
+
signal,
|
|
79
80
|
});
|
|
80
81
|
return {
|
|
81
82
|
...result,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
2
|
-
import { doLaunchOrthologs } from './doLaunchOrthologs';
|
|
3
2
|
import { launchMSA } from '../utils/msa';
|
|
4
3
|
import { defaultMaxSpecies, fetchOrthologRows, fetchProteinForGene, resolveGeneId, } from '../utils/ncbiOrthologs';
|
|
5
4
|
import { fetchPantherOrthologs } from '../utils/pantherOrthologs';
|
|
6
5
|
import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
|
|
6
|
+
import { doLaunchOrthologs } from './doLaunchOrthologs';
|
|
7
7
|
// Every network call is mocked and nothing else is. What is under test is the
|
|
8
8
|
// argument shaping either side of those calls -- which species get asked for,
|
|
9
9
|
// what becomes the QUERY row, and whether the row earns the Accession that
|
|
@@ -33,10 +33,24 @@ const setQuerySeqName = vi.fn();
|
|
|
33
33
|
function makeModel(orthologParams) {
|
|
34
34
|
return {
|
|
35
35
|
orthologParams,
|
|
36
|
-
setProgress: () => { },
|
|
37
36
|
setQuerySeqName,
|
|
38
37
|
};
|
|
39
38
|
}
|
|
39
|
+
// a scope that never cancels, so these tests see the launch's own behaviour --
|
|
40
|
+
// what the scope does when it IS cancelled is runLaunch.test.ts's subject
|
|
41
|
+
function launch({ self }) {
|
|
42
|
+
return doLaunchOrthologs({
|
|
43
|
+
self,
|
|
44
|
+
scope: {
|
|
45
|
+
signal: new AbortController().signal,
|
|
46
|
+
act: fn => {
|
|
47
|
+
fn();
|
|
48
|
+
},
|
|
49
|
+
onProgress: () => { },
|
|
50
|
+
onRid: () => { },
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
40
54
|
function params(extra = {}) {
|
|
41
55
|
return {
|
|
42
56
|
taxId: HUMAN,
|
|
@@ -79,11 +93,11 @@ beforeEach(() => {
|
|
|
79
93
|
});
|
|
80
94
|
describe('which species become rows', () => {
|
|
81
95
|
test('omitted taxa asks for no restriction at all, which is every ortholog NCBI has', async () => {
|
|
82
|
-
await
|
|
96
|
+
await launch({ self: makeModel(params()) });
|
|
83
97
|
expect(rowRequest().taxa).toBeUndefined();
|
|
84
98
|
});
|
|
85
99
|
test('given taxa is taken as written', async () => {
|
|
86
|
-
await
|
|
100
|
+
await launch({
|
|
87
101
|
self: makeModel(params({ taxa: [HUMAN, 10090, 9615] })),
|
|
88
102
|
});
|
|
89
103
|
expect(rowRequest().taxa).toEqual([9606, 9615, 10090]);
|
|
@@ -92,20 +106,20 @@ describe('which species become rows', () => {
|
|
|
92
106
|
// and "the query row already covers this one" stay separable -- an unrestricted
|
|
93
107
|
// launch still has to drop the query species.
|
|
94
108
|
test('the query species is excluded whether or not taxa was given', async () => {
|
|
95
|
-
await
|
|
109
|
+
await launch({ self: makeModel(params()) });
|
|
96
110
|
expect(rowRequest().exclude).toBe(HUMAN);
|
|
97
111
|
vi.clearAllMocks();
|
|
98
112
|
mockResolveGeneId.mockResolvedValue({ geneId: GENE_ID, matched: 'NLRP1' });
|
|
99
113
|
mockFetchProtein.mockResolvedValue(REPRESENTATIVE);
|
|
100
114
|
mockFetchRows.mockResolvedValue([]);
|
|
101
115
|
mockLaunchMSA.mockResolvedValue({ msa: '', tree: '' });
|
|
102
|
-
await
|
|
116
|
+
await launch({
|
|
103
117
|
self: makeModel(params({ taxa: [HUMAN, 10090] })),
|
|
104
118
|
});
|
|
105
119
|
expect(rowRequest().exclude).toBe(HUMAN);
|
|
106
120
|
});
|
|
107
121
|
test('an empty list is a request for no rows, not a request for all of them', async () => {
|
|
108
|
-
await
|
|
122
|
+
await launch({ self: makeModel(params({ taxa: [] })) });
|
|
109
123
|
expect(rowRequest().taxa).toEqual([]);
|
|
110
124
|
});
|
|
111
125
|
});
|
|
@@ -114,11 +128,11 @@ describe('which species become rows', () => {
|
|
|
114
128
|
// second a row.
|
|
115
129
|
describe('the row cap', () => {
|
|
116
130
|
test('is passed through when given', async () => {
|
|
117
|
-
await
|
|
131
|
+
await launch({ self: makeModel(params({ maxSpecies: 12 })) });
|
|
118
132
|
expect(rowRequest().limit).toBe(12);
|
|
119
133
|
});
|
|
120
134
|
test('omitted leaves the default to fetchOrthologGenes rather than sending Infinity', async () => {
|
|
121
|
-
await
|
|
135
|
+
await launch({ self: makeModel(params()) });
|
|
122
136
|
expect(rowRequest().limit).toBeUndefined();
|
|
123
137
|
expect(defaultMaxSpecies).toBeGreaterThan(2);
|
|
124
138
|
});
|
|
@@ -129,7 +143,7 @@ describe('the row cap', () => {
|
|
|
129
143
|
// model's querySeqName and the header have to be the same string.
|
|
130
144
|
describe('the query row name', () => {
|
|
131
145
|
test('is the species, marked, rather than a bare QUERY among named rows', async () => {
|
|
132
|
-
await
|
|
146
|
+
await launch({ self: makeModel(params()) });
|
|
133
147
|
expect(queryRowName()).toBe('human_query');
|
|
134
148
|
expect(setQuerySeqName).toHaveBeenCalledWith('human_query');
|
|
135
149
|
});
|
|
@@ -137,41 +151,41 @@ describe('the query row name', () => {
|
|
|
137
151
|
mockFetchRows.mockResolvedValue([
|
|
138
152
|
{ label: 'human_query', sequence: 'MM' },
|
|
139
153
|
]);
|
|
140
|
-
await
|
|
154
|
+
await launch({ self: makeModel(params()) });
|
|
141
155
|
expect(queryRowName()).toBe('human_query_2');
|
|
142
156
|
expect(setQuerySeqName).toHaveBeenCalledWith('human_query_2');
|
|
143
157
|
});
|
|
144
158
|
test('falls back rather than throwing when NCBI cannot name the taxon', async () => {
|
|
145
159
|
vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
146
160
|
mockFetchTaxonomy.mockRejectedValue(new Error('429'));
|
|
147
|
-
await
|
|
161
|
+
await launch({ self: makeModel(params()) });
|
|
148
162
|
expect(queryRowName()).toBe('query_query');
|
|
149
163
|
});
|
|
150
164
|
test('the metadata that drives the domain overlay is keyed to that same name', async () => {
|
|
151
|
-
const result = await
|
|
165
|
+
const result = await launch({ self: makeModel(params()) });
|
|
152
166
|
expect(Object.keys(JSON.parse(result.treeMetadata))).toContain('human_query');
|
|
153
167
|
});
|
|
154
168
|
});
|
|
155
169
|
describe('the query row sequence', () => {
|
|
156
170
|
test('omitted proteinSequence falls back to the representative protein', async () => {
|
|
157
|
-
await
|
|
171
|
+
await launch({ self: makeModel(params()) });
|
|
158
172
|
expect(queryRowSent()).toBe(REPRESENTATIVE.sequence);
|
|
159
173
|
});
|
|
160
174
|
test('a supplied sequence is used, and is cleaned first', async () => {
|
|
161
|
-
await
|
|
175
|
+
await launch({
|
|
162
176
|
self: makeModel(params({ proteinSequence: 'MAGG*AWGR&' })),
|
|
163
177
|
});
|
|
164
178
|
expect(queryRowSent()).toBe('MAGGAWGR');
|
|
165
179
|
});
|
|
166
180
|
test('throws when neither a sequence nor a representative is available', async () => {
|
|
167
181
|
mockFetchProtein.mockResolvedValue(undefined);
|
|
168
|
-
await expect(
|
|
182
|
+
await expect(launch({ self: makeModel(params()) })).rejects.toThrow(/No query protein/);
|
|
169
183
|
expect(mockLaunchMSA).not.toHaveBeenCalled();
|
|
170
184
|
});
|
|
171
185
|
test('a failed representative lookup does not take down a launch that brought its own sequence', async () => {
|
|
172
186
|
vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
173
187
|
mockFetchProtein.mockRejectedValue(new Error('429'));
|
|
174
|
-
await
|
|
188
|
+
await launch({
|
|
175
189
|
self: makeModel(params({ proteinSequence: REPRESENTATIVE.sequence })),
|
|
176
190
|
});
|
|
177
191
|
expect(queryRowSent()).toBe(REPRESENTATIVE.sequence);
|
|
@@ -183,14 +197,14 @@ describe('the query row sequence', () => {
|
|
|
183
197
|
// here, in both directions.
|
|
184
198
|
describe('the Accession that drives the domain overlay', () => {
|
|
185
199
|
test('is attached when the query row IS the representative protein', async () => {
|
|
186
|
-
const result = await
|
|
200
|
+
const result = await launch({ self: makeModel(params()) });
|
|
187
201
|
expect(queryMetadata(result)).toMatchObject({
|
|
188
202
|
'Gene ID': GENE_ID,
|
|
189
203
|
Accession: REPRESENTATIVE.accession,
|
|
190
204
|
});
|
|
191
205
|
});
|
|
192
206
|
test('is withheld from a non-representative isoform', async () => {
|
|
193
|
-
const result = await
|
|
207
|
+
const result = await launch({
|
|
194
208
|
self: makeModel(params({ proteinSequence: 'MDIFFERENTISOFORM' })),
|
|
195
209
|
});
|
|
196
210
|
expect(queryMetadata(result).Accession).toBeUndefined();
|
|
@@ -198,7 +212,7 @@ describe('the Accession that drives the domain overlay', () => {
|
|
|
198
212
|
test('is withheld when the representative lookup failed', async () => {
|
|
199
213
|
vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
200
214
|
mockFetchProtein.mockRejectedValue(new Error('429'));
|
|
201
|
-
const result = await
|
|
215
|
+
const result = await launch({
|
|
202
216
|
self: makeModel(params({ proteinSequence: REPRESENTATIVE.sequence })),
|
|
203
217
|
});
|
|
204
218
|
expect(queryMetadata(result).Accession).toBeUndefined();
|
|
@@ -235,12 +249,12 @@ describe('the PANTHER source', () => {
|
|
|
235
249
|
mockFetchTaxonomy.mockResolvedValue(new Map([[YEAST, { sciname: 'Saccharomyces cerevisiae' }]]));
|
|
236
250
|
});
|
|
237
251
|
test('source omitted is NCBI, so an old launch never reaches PANTHER', async () => {
|
|
238
|
-
await
|
|
252
|
+
await launch({ self: makeModel(params()) });
|
|
239
253
|
expect(mockFetchPanther).not.toHaveBeenCalled();
|
|
240
254
|
expect(mockResolveGeneId).toHaveBeenCalled();
|
|
241
255
|
});
|
|
242
256
|
test('source panther asks PANTHER with the same species semantics, and skips NCBI', async () => {
|
|
243
|
-
await
|
|
257
|
+
await launch({
|
|
244
258
|
self: makeModel({
|
|
245
259
|
taxId: YEAST,
|
|
246
260
|
source: 'panther',
|
|
@@ -260,7 +274,7 @@ describe('the PANTHER source', () => {
|
|
|
260
274
|
expect(limit).toBe(7);
|
|
261
275
|
});
|
|
262
276
|
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
|
|
277
|
+
const result = await launch({
|
|
264
278
|
self: makeModel({
|
|
265
279
|
taxId: YEAST,
|
|
266
280
|
source: 'panther',
|
|
@@ -280,7 +294,7 @@ describe('the PANTHER source', () => {
|
|
|
280
294
|
});
|
|
281
295
|
});
|
|
282
296
|
test('a supplied sequence still wins, and a different isoform earns no Accession', async () => {
|
|
283
|
-
const result = await
|
|
297
|
+
const result = await launch({
|
|
284
298
|
self: makeModel({
|
|
285
299
|
taxId: YEAST,
|
|
286
300
|
source: 'panther',
|
|
@@ -294,7 +308,7 @@ describe('the PANTHER source', () => {
|
|
|
294
308
|
});
|
|
295
309
|
test('names PANTHER when it has no protein for the query row', async () => {
|
|
296
310
|
mockFetchPanther.mockResolvedValue({ ...found, query: undefined });
|
|
297
|
-
await expect(
|
|
311
|
+
await expect(launch({
|
|
298
312
|
self: makeModel({
|
|
299
313
|
taxId: YEAST,
|
|
300
314
|
source: 'panther',
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { getSession } from '@jbrowse/core/util';
|
|
2
|
-
import { hasHoverPosition } from './util';
|
|
2
|
+
import { hasHoverPosition, hasQueryRow } from './util';
|
|
3
3
|
export function genomeToMSA({ model }) {
|
|
4
4
|
const { hovered } = getSession(model);
|
|
5
5
|
const { querySeqName, transcriptToMsaMap, connectedView, mafRegion } = model;
|
|
6
|
-
if (!connectedView?.initialized ||
|
|
6
|
+
if (!connectedView?.initialized ||
|
|
7
|
+
!hasHoverPosition(hovered) ||
|
|
8
|
+
!hasQueryRow(model)) {
|
|
7
9
|
return undefined;
|
|
8
10
|
}
|
|
9
11
|
const { coord, refName } = hovered.hoverPosition;
|
|
@@ -19,6 +19,7 @@ describe('genomeToMSA', () => {
|
|
|
19
19
|
});
|
|
20
20
|
const model = {
|
|
21
21
|
querySeqName: 'hg38.chr1',
|
|
22
|
+
rows: [['hg38.chr1', 'ACGTACGTAC']],
|
|
22
23
|
transcriptToMsaMap: undefined,
|
|
23
24
|
mafRegion: {
|
|
24
25
|
refName: 'chr1',
|
|
@@ -38,6 +39,7 @@ describe('genomeToMSA', () => {
|
|
|
38
39
|
});
|
|
39
40
|
const model = {
|
|
40
41
|
querySeqName: 'hg38.chr1',
|
|
42
|
+
rows: [['hg38.chr1', 'ACGTACGTAC']],
|
|
41
43
|
transcriptToMsaMap: undefined,
|
|
42
44
|
mafRegion: {
|
|
43
45
|
refName: 'chr1',
|
|
@@ -62,6 +64,7 @@ describe('genomeToMSA', () => {
|
|
|
62
64
|
const mockSeqPosToVisibleCol = vi.fn().mockReturnValue(5);
|
|
63
65
|
const model = {
|
|
64
66
|
querySeqName: 'hg38.chr1',
|
|
67
|
+
rows: [['hg38.chr1', 'ACGTACGTAC']],
|
|
65
68
|
transcriptToMsaMap: undefined,
|
|
66
69
|
mafRegion: {
|
|
67
70
|
refName: 'chr1',
|
|
@@ -90,6 +93,7 @@ describe('genomeToMSA', () => {
|
|
|
90
93
|
});
|
|
91
94
|
const model = {
|
|
92
95
|
querySeqName: 'hg38.chr1',
|
|
96
|
+
rows: [['hg38.chr1', 'ACGTACGTAC']],
|
|
93
97
|
transcriptToMsaMap: undefined,
|
|
94
98
|
mafRegion: {
|
|
95
99
|
refName: 'chr1',
|
|
@@ -116,6 +120,7 @@ describe('genomeToMSA', () => {
|
|
|
116
120
|
});
|
|
117
121
|
const model = {
|
|
118
122
|
querySeqName: 'hg38.chr1',
|
|
123
|
+
rows: [['hg38.chr1', 'ACGTACGTAC']],
|
|
119
124
|
transcriptToMsaMap: undefined,
|
|
120
125
|
mafRegion: {
|
|
121
126
|
refName: 'chr1',
|
|
@@ -142,6 +147,7 @@ describe('genomeToMSA', () => {
|
|
|
142
147
|
});
|
|
143
148
|
const model = {
|
|
144
149
|
querySeqName: 'hg38.chr1',
|
|
150
|
+
rows: [['hg38.chr1', 'ACGTACGTAC']],
|
|
145
151
|
transcriptToMsaMap: undefined,
|
|
146
152
|
mafRegion: {
|
|
147
153
|
refName: 'chr1',
|
|
@@ -167,6 +173,7 @@ describe('genomeToMSA', () => {
|
|
|
167
173
|
});
|
|
168
174
|
const model = {
|
|
169
175
|
querySeqName: 'hg38.chr1',
|
|
176
|
+
rows: [['hg38.chr1', 'ACGTACGTAC']],
|
|
170
177
|
transcriptToMsaMap: undefined,
|
|
171
178
|
mafRegion: {
|
|
172
179
|
refName: 'chr1',
|
|
@@ -195,6 +202,7 @@ describe('genomeToMSA', () => {
|
|
|
195
202
|
const mockSeqPosToVisibleCol = vi.fn().mockReturnValue(10);
|
|
196
203
|
const model = {
|
|
197
204
|
querySeqName: 'QUERY',
|
|
205
|
+
rows: [['QUERY', 'MKVLTAEEK']],
|
|
198
206
|
transcriptToMsaMap: {
|
|
199
207
|
refName: 'chr1',
|
|
200
208
|
// g2p is keyed by 0-based genome position, the hover coord is 1-based
|
|
@@ -220,6 +228,7 @@ describe('genomeToMSA', () => {
|
|
|
220
228
|
const mockSeqPosToVisibleCol = vi.fn();
|
|
221
229
|
const model = {
|
|
222
230
|
querySeqName: 'QUERY',
|
|
231
|
+
rows: [['QUERY', 'MKVLTAEEK']],
|
|
223
232
|
transcriptToMsaMap: {
|
|
224
233
|
refName: 'chr1',
|
|
225
234
|
g2p: { 1004: 10 },
|
|
@@ -240,6 +249,7 @@ describe('genomeToMSA', () => {
|
|
|
240
249
|
});
|
|
241
250
|
const model = {
|
|
242
251
|
querySeqName: 'QUERY',
|
|
252
|
+
rows: [['QUERY', 'MKVLTAEEK']],
|
|
243
253
|
transcriptToMsaMap: {
|
|
244
254
|
refName: 'chr1',
|
|
245
255
|
g2p: { 1000: 0 }, // No entry for 1004
|
|
@@ -261,6 +271,7 @@ describe('genomeToMSA', () => {
|
|
|
261
271
|
});
|
|
262
272
|
const model = {
|
|
263
273
|
querySeqName: 'QUERY',
|
|
274
|
+
rows: [['QUERY', 'MKVLTAEEK']],
|
|
264
275
|
transcriptToMsaMap: undefined,
|
|
265
276
|
mafRegion: undefined,
|
|
266
277
|
connectedView: { initialized: true },
|
|
@@ -269,4 +280,27 @@ describe('genomeToMSA', () => {
|
|
|
269
280
|
const result = genomeToMSA({ model });
|
|
270
281
|
expect(result).toBeUndefined();
|
|
271
282
|
});
|
|
283
|
+
// seqPosToVisibleCol answers 0 for a row name it does not know, so without a
|
|
284
|
+
// guard an alignment whose query row is missing -- the default 'QUERY' on an
|
|
285
|
+
// uploaded file, or the empty name the manual panel leaves when it matches
|
|
286
|
+
// nothing -- lights column 0 on every genome hover
|
|
287
|
+
test('returns undefined when querySeqName names no row here', () => {
|
|
288
|
+
mockGetSession.mockReturnValue({
|
|
289
|
+
hovered: {
|
|
290
|
+
hoverFeature: {},
|
|
291
|
+
hoverPosition: { coord: 1005, refName: 'chr1' },
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
const seqPosToVisibleCol = vi.fn(() => 0);
|
|
295
|
+
const model = {
|
|
296
|
+
querySeqName: 'QUERY',
|
|
297
|
+
rows: [['some_other_row', 'MKVLTAEEK']],
|
|
298
|
+
transcriptToMsaMap: { refName: 'chr1', g2p: { 1004: 3 } },
|
|
299
|
+
mafRegion: undefined,
|
|
300
|
+
connectedView: { initialized: true },
|
|
301
|
+
seqPosToVisibleCol,
|
|
302
|
+
};
|
|
303
|
+
expect(genomeToMSA({ model })).toBeUndefined();
|
|
304
|
+
expect(seqPosToVisibleCol).not.toHaveBeenCalled();
|
|
305
|
+
});
|
|
272
306
|
});
|