jbrowse-plugin-msaview 2.8.2 → 2.10.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/{NCBIBlastQuery/NCBIBlastRIDPanel.d.ts → BlastQuery/BlastAutomaticPanel.d.ts} +2 -3
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastAutomaticPanel.js → BlastQuery/BlastAutomaticPanel.js} +6 -25
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastManualPanel.d.ts → BlastQuery/BlastManualPanel.d.ts} +2 -3
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastManualPanel.js → BlastQuery/BlastManualPanel.js} +7 -4
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastMethodSelector.d.ts → BlastQuery/BlastMethodSelector.d.ts} +2 -2
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastMethodSelector.js → BlastQuery/BlastMethodSelector.js} +1 -2
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastPanel.d.ts → BlastQuery/BlastPanel.d.ts} +3 -12
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastPanel.js → BlastQuery/BlastPanel.js} +14 -16
- package/dist/LaunchMsaView/components/BlastQuery/BlastSettingsDialog.d.ts +8 -0
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBISettingsDialog.js → BlastQuery/BlastSettingsDialog.js} +13 -9
- package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/CachedBlastResults.js +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +12 -0
- package/dist/LaunchMsaView/components/BlastQuery/consts.js +17 -0
- package/dist/LaunchMsaView/components/LaunchMsaViewDialog.js +3 -3
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +15 -35
- package/dist/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.d.ts +15 -0
- package/dist/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.js +67 -0
- package/dist/LaunchMsaView/index.js +19 -1
- package/dist/MsaViewPanel/components/JobLink.d.ts +5 -0
- package/dist/MsaViewPanel/components/JobLink.js +13 -0
- package/dist/MsaViewPanel/components/LoadingBLAST.d.ts +1 -2
- package/dist/MsaViewPanel/components/LoadingBLAST.js +8 -15
- package/dist/MsaViewPanel/components/MsaViewPanel.js +1 -1
- package/dist/MsaViewPanel/doLaunchBlast.js +11 -19
- package/dist/MsaViewPanel/doLaunchOrthologs.js +43 -12
- package/dist/MsaViewPanel/doLaunchOrthologs.test.d.ts +1 -0
- package/dist/MsaViewPanel/doLaunchOrthologs.test.js +201 -0
- package/dist/MsaViewPanel/model.d.ts +20 -12
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +31 -33
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/blastCache.d.ts +8 -4
- package/dist/utils/blastCache.js +4 -5
- package/dist/utils/ebiBlast.d.ts +52 -0
- package/dist/utils/ebiBlast.js +63 -0
- package/dist/utils/ebiJobDispatcher.d.ts +33 -0
- package/dist/utils/ebiJobDispatcher.js +80 -0
- package/dist/utils/ebiJobDispatcher.test.d.ts +1 -0
- package/dist/utils/ebiJobDispatcher.test.js +46 -0
- package/dist/utils/eutils.d.ts +9 -0
- package/dist/utils/eutils.js +18 -0
- package/dist/utils/fetch.js +24 -1
- package/dist/utils/msa.d.ts +1 -1
- package/dist/utils/msa.js +25 -32
- package/dist/utils/ncbiOrthologs.d.ts +17 -75
- package/dist/utils/ncbiOrthologs.js +67 -63
- package/dist/utils/ncbiOrthologs.test.js +82 -2
- package/dist/utils/ncbiTaxonomy.d.ts +11 -0
- package/dist/utils/ncbiTaxonomy.js +33 -0
- package/dist/utils/types.d.ts +9 -14
- package/dist/utils/useLocalStorage.d.ts +1 -0
- package/dist/utils/useLocalStorage.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastAutomaticPanel.tsx → BlastQuery/BlastAutomaticPanel.tsx} +13 -59
- package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastManualPanel.tsx → BlastQuery/BlastManualPanel.tsx} +7 -6
- package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastMethodSelector.tsx → BlastQuery/BlastMethodSelector.tsx} +2 -7
- package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastPanel.tsx → BlastQuery/BlastPanel.tsx} +21 -25
- package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBISettingsDialog.tsx → BlastQuery/BlastSettingsDialog.tsx} +26 -13
- package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/CachedBlastResults.tsx +1 -1
- package/src/LaunchMsaView/components/BlastQuery/consts.ts +22 -0
- package/src/LaunchMsaView/components/LaunchMsaViewDialog.tsx +5 -7
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +35 -73
- package/src/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.tsx +92 -0
- package/src/LaunchMsaView/index.ts +20 -2
- package/src/MsaViewPanel/components/JobLink.tsx +17 -0
- package/src/MsaViewPanel/components/LoadingBLAST.tsx +10 -41
- package/src/MsaViewPanel/components/MsaViewPanel.tsx +1 -1
- package/src/MsaViewPanel/doLaunchBlast.ts +12 -28
- package/src/MsaViewPanel/doLaunchOrthologs.test.ts +247 -0
- package/src/MsaViewPanel/doLaunchOrthologs.ts +56 -13
- package/src/MsaViewPanel/model.ts +10 -7
- package/src/utils/blastCache.ts +8 -9
- package/src/utils/ebiBlast.ts +114 -0
- package/src/utils/ebiJobDispatcher.test.ts +54 -0
- package/src/utils/ebiJobDispatcher.ts +120 -0
- package/src/utils/eutils.ts +19 -0
- package/src/utils/fetch.ts +26 -1
- package/src/utils/msa.ts +26 -47
- package/src/utils/ncbiOrthologs.test.ts +96 -2
- package/src/utils/ncbiOrthologs.ts +83 -71
- package/src/utils/ncbiTaxonomy.ts +37 -0
- package/src/utils/types.ts +8 -13
- package/src/utils/useLocalStorage.ts +1 -1
- package/src/version.ts +1 -1
- package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastAutomaticPanel.d.ts +0 -10
- package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastRIDPanel.js +0 -74
- package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBISettingsDialog.d.ts +0 -5
- package/dist/LaunchMsaView/components/NCBIBlastQuery/consts.d.ts +0 -7
- package/dist/LaunchMsaView/components/NCBIBlastQuery/consts.js +0 -4
- package/dist/MsaViewPanel/components/RIDLink.d.ts +0 -6
- package/dist/MsaViewPanel/components/RIDLink.js +0 -12
- package/dist/utils/ncbiBlast.d.ts +0 -30
- package/dist/utils/ncbiBlast.js +0 -84
- package/src/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastRIDPanel.tsx +0 -137
- package/src/LaunchMsaView/components/NCBIBlastQuery/consts.ts +0 -10
- package/src/MsaViewPanel/components/RIDLink.tsx +0 -19
- package/src/utils/ncbiBlast.ts +0 -143
- /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/CachedBlastResults.d.ts +0 -0
- /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/MsaAlgorithmSelect.d.ts +0 -0
- /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/MsaAlgorithmSelect.js +0 -0
- /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/blastLaunchView.d.ts +0 -0
- /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/blastLaunchView.js +0 -0
- /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/useCachedBlastResults.d.ts +0 -0
- /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/useCachedBlastResults.js +0 -0
- /package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/MsaAlgorithmSelect.tsx +0 -0
- /package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/blastLaunchView.ts +0 -0
- /package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/useCachedBlastResults.ts +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { makeId, strip } from '../LaunchMsaView/components/util'
|
|
2
2
|
import { cleanProteinSequence } from '../LaunchMsaView/util'
|
|
3
3
|
import { saveBlastResult } from '../utils/blastCache'
|
|
4
|
+
import { queryEbiBlast } from '../utils/ebiBlast'
|
|
4
5
|
import { launchMSA } from '../utils/msa'
|
|
5
|
-
import { queryBlast, queryBlastFromRid } from '../utils/ncbiBlast'
|
|
6
6
|
import { fetchTaxonomyInfo } from '../utils/taxonomyNames'
|
|
7
7
|
|
|
8
8
|
import type { JBrowsePluginMsaViewModel } from './model'
|
|
@@ -14,39 +14,24 @@ export async function doLaunchBlast({
|
|
|
14
14
|
}: {
|
|
15
15
|
self: JBrowsePluginMsaViewModel
|
|
16
16
|
}) {
|
|
17
|
-
const {
|
|
18
|
-
|
|
19
|
-
blastDatabase,
|
|
20
|
-
blastProgram,
|
|
21
|
-
msaAlgorithm,
|
|
22
|
-
proteinSequence,
|
|
23
|
-
selectedTranscript,
|
|
24
|
-
rid: existingRid,
|
|
25
|
-
} = self.blastParams!
|
|
17
|
+
const { blastDatabase, msaAlgorithm, proteinSequence, selectedTranscript } =
|
|
18
|
+
self.blastParams!
|
|
26
19
|
const cleanedSeq = cleanProteinSequence(proteinSequence)
|
|
27
20
|
|
|
28
21
|
const onProgress = (arg: string) => {
|
|
29
22
|
self.setProgress(arg)
|
|
30
23
|
}
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
const { hits, rid } = await queryEbiBlast({
|
|
26
|
+
query: cleanedSeq,
|
|
27
|
+
blastDatabase,
|
|
28
|
+
onProgress,
|
|
29
|
+
// publish the job id before the first poll so the view can link out while
|
|
34
30
|
// the job is still running
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
? await queryBlastFromRid({ rid: existingRid, baseUrl, onProgress })
|
|
40
|
-
: await queryBlast({
|
|
41
|
-
query: cleanedSeq,
|
|
42
|
-
blastDatabase,
|
|
43
|
-
blastProgram,
|
|
44
|
-
baseUrl,
|
|
45
|
-
onProgress,
|
|
46
|
-
onRid: r => {
|
|
47
|
-
self.setRid(r)
|
|
48
|
-
},
|
|
49
|
-
})
|
|
31
|
+
onRid: r => {
|
|
32
|
+
self.setRid(r)
|
|
33
|
+
},
|
|
34
|
+
})
|
|
50
35
|
|
|
51
36
|
self.setProgress('Fetching species taxonomy info...')
|
|
52
37
|
const taxids = hits
|
|
@@ -81,7 +66,6 @@ export async function doLaunchBlast({
|
|
|
81
66
|
await saveBlastResult({
|
|
82
67
|
proteinSequence: cleanedSeq,
|
|
83
68
|
blastDatabase,
|
|
84
|
-
blastProgram,
|
|
85
69
|
msaAlgorithm,
|
|
86
70
|
msa: result.msa,
|
|
87
71
|
tree: result.tree,
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { doLaunchOrthologs } from './doLaunchOrthologs'
|
|
4
|
+
import { launchMSA } from '../utils/msa'
|
|
5
|
+
import {
|
|
6
|
+
defaultMaxSpecies,
|
|
7
|
+
fetchOrthologRows,
|
|
8
|
+
fetchProteinForGene,
|
|
9
|
+
resolveGeneId,
|
|
10
|
+
} from '../utils/ncbiOrthologs'
|
|
11
|
+
import { fetchTaxonomyInfo } from '../utils/taxonomyNames'
|
|
12
|
+
|
|
13
|
+
import type { JBrowsePluginMsaViewModel } from './model'
|
|
14
|
+
import type { OrthologRow } from '../utils/ncbiOrthologs'
|
|
15
|
+
|
|
16
|
+
// Every network call is mocked and nothing else is. What is under test is the
|
|
17
|
+
// argument shaping either side of those calls -- which species get asked for,
|
|
18
|
+
// what becomes the QUERY row, and whether the row earns the Accession that
|
|
19
|
+
// drives the CDD overlay -- so the real cleanProteinSequence stays in the
|
|
20
|
+
// picture.
|
|
21
|
+
vi.mock('../utils/ncbiOrthologs', async importOriginal => ({
|
|
22
|
+
...(await importOriginal<Record<string, unknown>>()),
|
|
23
|
+
resolveGeneId: vi.fn(),
|
|
24
|
+
fetchProteinForGene: vi.fn(),
|
|
25
|
+
fetchOrthologRows: vi.fn(),
|
|
26
|
+
}))
|
|
27
|
+
vi.mock('../utils/msa', () => ({ launchMSA: vi.fn() }))
|
|
28
|
+
vi.mock('../utils/taxonomyNames', () => ({ fetchTaxonomyInfo: vi.fn() }))
|
|
29
|
+
|
|
30
|
+
const mockResolveGeneId = vi.mocked(resolveGeneId)
|
|
31
|
+
const mockFetchProtein = vi.mocked(fetchProteinForGene)
|
|
32
|
+
const mockFetchRows = vi.mocked(fetchOrthologRows)
|
|
33
|
+
const mockLaunchMSA = vi.mocked(launchMSA)
|
|
34
|
+
const mockFetchTaxonomy = vi.mocked(fetchTaxonomyInfo)
|
|
35
|
+
|
|
36
|
+
const HUMAN = 9606
|
|
37
|
+
const GENE_ID = '22861'
|
|
38
|
+
const REPRESENTATIVE = { accession: 'NP_127497.1', sequence: 'MAGGAWGRLACY' }
|
|
39
|
+
|
|
40
|
+
const setQuerySeqName = vi.fn()
|
|
41
|
+
|
|
42
|
+
function makeModel(orthologParams: Record<string, unknown>) {
|
|
43
|
+
return {
|
|
44
|
+
orthologParams,
|
|
45
|
+
setProgress: () => {},
|
|
46
|
+
setQuerySeqName,
|
|
47
|
+
} as unknown as JBrowsePluginMsaViewModel
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function params(extra: Record<string, unknown> = {}) {
|
|
51
|
+
return {
|
|
52
|
+
taxId: HUMAN,
|
|
53
|
+
geneCandidates: ['NLRP1'],
|
|
54
|
+
msaAlgorithm: 'clustalo',
|
|
55
|
+
...extra,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// What fetchOrthologRows was asked for, which is the only place the species
|
|
60
|
+
// defaults are observable.
|
|
61
|
+
function rowRequest() {
|
|
62
|
+
const { taxa, exclude, limit } = mockFetchRows.mock.calls[0]![0]
|
|
63
|
+
return {
|
|
64
|
+
taxa: taxa && [...taxa].sort((a, b) => a - b),
|
|
65
|
+
exclude,
|
|
66
|
+
limit,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The QUERY row as it went to the aligner, read back out of the FASTA rather
|
|
71
|
+
// than out of an intermediate, since the FASTA is what the alignment is of.
|
|
72
|
+
function queryRowSent() {
|
|
73
|
+
return mockLaunchMSA.mock.calls[0]![0].sequence.split('\n')[1]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Keyed by the row's own label, which has to be the name the FASTA header
|
|
77
|
+
// carries -- the tree comes back from the aligner naming its leaves that way,
|
|
78
|
+
// and the metadata is paired to a leaf by name.
|
|
79
|
+
function queryMetadata(result: { treeMetadata: string }) {
|
|
80
|
+
return JSON.parse(result.treeMetadata)[queryRowName()] as Record<
|
|
81
|
+
string,
|
|
82
|
+
string
|
|
83
|
+
>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function queryRowName() {
|
|
87
|
+
return mockLaunchMSA.mock.calls[0]![0].sequence.split('\n')[0]!.slice(1)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
vi.clearAllMocks()
|
|
92
|
+
mockResolveGeneId.mockResolvedValue({ geneId: GENE_ID, matched: 'NLRP1' })
|
|
93
|
+
mockFetchProtein.mockResolvedValue(REPRESENTATIVE)
|
|
94
|
+
mockFetchRows.mockResolvedValue([] as OrthologRow[])
|
|
95
|
+
mockLaunchMSA.mockResolvedValue({ msa: '', tree: '' })
|
|
96
|
+
mockFetchTaxonomy.mockResolvedValue(
|
|
97
|
+
new Map([[HUMAN, { sciname: 'Homo sapiens', commonName: 'human' }]]),
|
|
98
|
+
)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('which species become rows', () => {
|
|
102
|
+
test('omitted taxa asks for no restriction at all, which is every ortholog NCBI has', async () => {
|
|
103
|
+
await doLaunchOrthologs({ self: makeModel(params()) })
|
|
104
|
+
expect(rowRequest().taxa).toBeUndefined()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('given taxa is taken as written', async () => {
|
|
108
|
+
await doLaunchOrthologs({
|
|
109
|
+
self: makeModel(params({ taxa: [HUMAN, 10090, 9615] })),
|
|
110
|
+
})
|
|
111
|
+
expect(rowRequest().taxa).toEqual([9606, 9615, 10090])
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// Not folded into the taxa list before the call, so that "restrict to these"
|
|
115
|
+
// and "the query row already covers this one" stay separable -- an unrestricted
|
|
116
|
+
// launch still has to drop the query species.
|
|
117
|
+
test('the query species is excluded whether or not taxa was given', async () => {
|
|
118
|
+
await doLaunchOrthologs({ self: makeModel(params()) })
|
|
119
|
+
expect(rowRequest().exclude).toBe(HUMAN)
|
|
120
|
+
vi.clearAllMocks()
|
|
121
|
+
mockResolveGeneId.mockResolvedValue({ geneId: GENE_ID, matched: 'NLRP1' })
|
|
122
|
+
mockFetchProtein.mockResolvedValue(REPRESENTATIVE)
|
|
123
|
+
mockFetchRows.mockResolvedValue([] as OrthologRow[])
|
|
124
|
+
mockLaunchMSA.mockResolvedValue({ msa: '', tree: '' })
|
|
125
|
+
await doLaunchOrthologs({
|
|
126
|
+
self: makeModel(params({ taxa: [HUMAN, 10090] })),
|
|
127
|
+
})
|
|
128
|
+
expect(rowRequest().exclude).toBe(HUMAN)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('an empty list is a request for no rows, not a request for all of them', async () => {
|
|
132
|
+
await doLaunchOrthologs({ self: makeModel(params({ taxa: [] })) })
|
|
133
|
+
expect(rowRequest().taxa).toEqual([])
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// The cap is the only thing standing between a launch and a 7 minute EBI job:
|
|
138
|
+
// NCBI publishes 865 orthologs for CFTR and the aligner runs at roughly half a
|
|
139
|
+
// second a row.
|
|
140
|
+
describe('the row cap', () => {
|
|
141
|
+
test('is passed through when given', async () => {
|
|
142
|
+
await doLaunchOrthologs({ self: makeModel(params({ maxSpecies: 12 })) })
|
|
143
|
+
expect(rowRequest().limit).toBe(12)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test('omitted leaves the default to fetchOrthologGenes rather than sending Infinity', async () => {
|
|
147
|
+
await doLaunchOrthologs({ self: makeModel(params()) })
|
|
148
|
+
expect(rowRequest().limit).toBeUndefined()
|
|
149
|
+
expect(defaultMaxSpecies).toBeGreaterThan(2)
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// The row's name is load bearing three times over: it is the FASTA header, it
|
|
154
|
+
// is therefore the tree leaf the aligner returns, and it is what
|
|
155
|
+
// `seqPosToVisibleCol` looks up to turn a genome hover into a column. So the
|
|
156
|
+
// model's querySeqName and the header have to be the same string.
|
|
157
|
+
describe('the query row name', () => {
|
|
158
|
+
test('is the species, marked, rather than a bare QUERY among named rows', async () => {
|
|
159
|
+
await doLaunchOrthologs({ self: makeModel(params()) })
|
|
160
|
+
expect(queryRowName()).toBe('human_query')
|
|
161
|
+
expect(setQuerySeqName).toHaveBeenCalledWith('human_query')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('cannot collide with an ortholog row that sanitizes to the same token', async () => {
|
|
165
|
+
mockFetchRows.mockResolvedValue([
|
|
166
|
+
{ label: 'human_query', sequence: 'MM' },
|
|
167
|
+
] as OrthologRow[])
|
|
168
|
+
await doLaunchOrthologs({ self: makeModel(params()) })
|
|
169
|
+
expect(queryRowName()).toBe('human_query_2')
|
|
170
|
+
expect(setQuerySeqName).toHaveBeenCalledWith('human_query_2')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('falls back rather than throwing when NCBI cannot name the taxon', async () => {
|
|
174
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
175
|
+
mockFetchTaxonomy.mockRejectedValue(new Error('429'))
|
|
176
|
+
await doLaunchOrthologs({ self: makeModel(params()) })
|
|
177
|
+
expect(queryRowName()).toBe('query_query')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('the metadata that drives the domain overlay is keyed to that same name', async () => {
|
|
181
|
+
const result = await doLaunchOrthologs({ self: makeModel(params()) })
|
|
182
|
+
expect(Object.keys(JSON.parse(result.treeMetadata))).toContain(
|
|
183
|
+
'human_query',
|
|
184
|
+
)
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
describe('the query row sequence', () => {
|
|
189
|
+
test('omitted proteinSequence falls back to the representative protein', async () => {
|
|
190
|
+
await doLaunchOrthologs({ self: makeModel(params()) })
|
|
191
|
+
expect(queryRowSent()).toBe(REPRESENTATIVE.sequence)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
test('a supplied sequence is used, and is cleaned first', async () => {
|
|
195
|
+
await doLaunchOrthologs({
|
|
196
|
+
self: makeModel(params({ proteinSequence: 'MAGG*AWGR&' })),
|
|
197
|
+
})
|
|
198
|
+
expect(queryRowSent()).toBe('MAGGAWGR')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
test('throws when neither a sequence nor a representative is available', async () => {
|
|
202
|
+
mockFetchProtein.mockResolvedValue(undefined)
|
|
203
|
+
await expect(
|
|
204
|
+
doLaunchOrthologs({ self: makeModel(params()) }),
|
|
205
|
+
).rejects.toThrow(/No query protein/)
|
|
206
|
+
expect(mockLaunchMSA).not.toHaveBeenCalled()
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test('a failed representative lookup does not take down a launch that brought its own sequence', async () => {
|
|
210
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
211
|
+
mockFetchProtein.mockRejectedValue(new Error('429'))
|
|
212
|
+
await doLaunchOrthologs({
|
|
213
|
+
self: makeModel(params({ proteinSequence: REPRESENTATIVE.sequence })),
|
|
214
|
+
})
|
|
215
|
+
expect(queryRowSent()).toBe(REPRESENTATIVE.sequence)
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
// The Accession is what afterCreateAutoruns.autoLoadProteinDomains keys the CDD
|
|
220
|
+
// overlay off, and attaching it to a row that is NOT the protein it names draws
|
|
221
|
+
// every domain box at an offset. So the byte-identity guard is the assertion
|
|
222
|
+
// here, in both directions.
|
|
223
|
+
describe('the Accession that drives the domain overlay', () => {
|
|
224
|
+
test('is attached when the query row IS the representative protein', async () => {
|
|
225
|
+
const result = await doLaunchOrthologs({ self: makeModel(params()) })
|
|
226
|
+
expect(queryMetadata(result)).toMatchObject({
|
|
227
|
+
'Gene ID': GENE_ID,
|
|
228
|
+
Accession: REPRESENTATIVE.accession,
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('is withheld from a non-representative isoform', async () => {
|
|
233
|
+
const result = await doLaunchOrthologs({
|
|
234
|
+
self: makeModel(params({ proteinSequence: 'MDIFFERENTISOFORM' })),
|
|
235
|
+
})
|
|
236
|
+
expect(queryMetadata(result).Accession).toBeUndefined()
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
test('is withheld when the representative lookup failed', async () => {
|
|
240
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
241
|
+
mockFetchProtein.mockRejectedValue(new Error('429'))
|
|
242
|
+
const result = await doLaunchOrthologs({
|
|
243
|
+
self: makeModel(params({ proteinSequence: REPRESENTATIVE.sequence })),
|
|
244
|
+
})
|
|
245
|
+
expect(queryMetadata(result).Accession).toBeUndefined()
|
|
246
|
+
})
|
|
247
|
+
})
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { cleanProteinSequence } from '../LaunchMsaView/util'
|
|
2
2
|
import { launchMSA } from '../utils/msa'
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
dedupeLabels,
|
|
5
5
|
fetchOrthologRows,
|
|
6
6
|
fetchProteinForGene,
|
|
7
7
|
resolveGeneId,
|
|
8
8
|
} from '../utils/ncbiOrthologs'
|
|
9
|
+
import { fetchTaxonomyInfo } from '../utils/taxonomyNames'
|
|
9
10
|
|
|
10
11
|
import type { JBrowsePluginMsaViewModel } from './model'
|
|
11
12
|
import type { OrthologRow } from '../utils/ncbiOrthologs'
|
|
@@ -30,8 +31,14 @@ export async function doLaunchOrthologs({
|
|
|
30
31
|
}: {
|
|
31
32
|
self: JBrowsePluginMsaViewModel
|
|
32
33
|
}) {
|
|
33
|
-
const {
|
|
34
|
-
|
|
34
|
+
const {
|
|
35
|
+
taxId,
|
|
36
|
+
taxa,
|
|
37
|
+
maxSpecies,
|
|
38
|
+
geneCandidates,
|
|
39
|
+
msaAlgorithm,
|
|
40
|
+
proteinSequence,
|
|
41
|
+
} = self.orthologParams!
|
|
35
42
|
|
|
36
43
|
const onProgress = (arg: string) => {
|
|
37
44
|
self.setProgress(arg)
|
|
@@ -61,21 +68,40 @@ export async function doLaunchOrthologs({
|
|
|
61
68
|
)
|
|
62
69
|
}
|
|
63
70
|
|
|
64
|
-
// Every species
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
const wantedTaxa = taxa ?? COMMON_SPECIES.map(s => s.taxId as number)
|
|
69
|
-
// the query species is represented by the query row above
|
|
70
|
-
const wanted = new Set(wantedTaxa.filter(t => t !== taxId))
|
|
71
|
+
// Every species NCBI has an ortholog for, when a launch names none, capped at
|
|
72
|
+
// maxSpecies. A launch that wants specific species lists them; one that just
|
|
73
|
+
// wants "this gene across species" gets NCBI's own order, which leads with the
|
|
74
|
+
// reference organisms.
|
|
71
75
|
const rows = await fetchOrthologRows({
|
|
72
76
|
geneId: resolved.geneId,
|
|
73
|
-
taxa:
|
|
77
|
+
taxa: taxa ? new Set(taxa) : undefined,
|
|
78
|
+
// the query species is represented by the query row above
|
|
79
|
+
exclude: taxId,
|
|
80
|
+
limit: maxSpecies,
|
|
74
81
|
onProgress,
|
|
75
82
|
})
|
|
76
83
|
|
|
84
|
+
// The query row is named for its species like every other row, with a suffix
|
|
85
|
+
// marking it as the one the genome view is linked to. A bare `QUERY` among
|
|
86
|
+
// ninety-nine named species reads as a row whose species failed to resolve,
|
|
87
|
+
// and there is nothing else in the picture saying which row the hover
|
|
88
|
+
// highlight travels through -- react-msaview has no notion of a query row, it
|
|
89
|
+
// only looks one up by name.
|
|
90
|
+
//
|
|
91
|
+
// Deduped against the ortholog labels rather than assumed unique: the query
|
|
92
|
+
// taxon is excluded from that set, but a subspecies can sanitize to the same
|
|
93
|
+
// token, and a collision would silently point the coordinate mapping at
|
|
94
|
+
// another animal's row.
|
|
95
|
+
const queryLabel = await queryRowLabel(taxId, rows)
|
|
96
|
+
self.setQuerySeqName(queryLabel)
|
|
97
|
+
|
|
77
98
|
const treeMetadata: Record<string, Record<string, string>> = {
|
|
78
|
-
|
|
99
|
+
[queryLabel]: buildQueryMetadata(
|
|
100
|
+
self,
|
|
101
|
+
resolved.geneId,
|
|
102
|
+
cleanedSeq,
|
|
103
|
+
representative,
|
|
104
|
+
),
|
|
79
105
|
}
|
|
80
106
|
for (const row of rows) {
|
|
81
107
|
treeMetadata[row.label] = buildRowMetadata(row)
|
|
@@ -84,7 +110,7 @@ export async function doLaunchOrthologs({
|
|
|
84
110
|
const result = await launchMSA({
|
|
85
111
|
algorithm: msaAlgorithm,
|
|
86
112
|
sequence: [
|
|
87
|
-
|
|
113
|
+
`>${queryLabel}\n${cleanedSeq}`,
|
|
88
114
|
...rows.map(r => `>${r.label}\n${r.sequence}`),
|
|
89
115
|
].join('\n'),
|
|
90
116
|
onProgress,
|
|
@@ -96,6 +122,23 @@ export async function doLaunchOrthologs({
|
|
|
96
122
|
}
|
|
97
123
|
}
|
|
98
124
|
|
|
125
|
+
/**
|
|
126
|
+
* `<species>_query`, unique against the ortholog labels. Falls back to the bare
|
|
127
|
+
* marker when NCBI cannot name the taxon, which is a naming failure and must not
|
|
128
|
+
* take down the launch.
|
|
129
|
+
*/
|
|
130
|
+
async function queryRowLabel(taxId: number, rows: OrthologRow[]) {
|
|
131
|
+
let name: string | undefined
|
|
132
|
+
try {
|
|
133
|
+
const info = (await fetchTaxonomyInfo([taxId])).get(taxId)
|
|
134
|
+
name = info?.commonName ?? info?.sciname
|
|
135
|
+
} catch (e) {
|
|
136
|
+
console.warn('[msaview-orthologs] taxonomy name lookup failed:', e)
|
|
137
|
+
}
|
|
138
|
+
return dedupeLabels([...rows.map(r => r.label), `${name ?? 'query'}_query`])
|
|
139
|
+
.at(-1)!
|
|
140
|
+
}
|
|
141
|
+
|
|
99
142
|
/**
|
|
100
143
|
* A failed lookup only costs the query row its domain overlay and, for a launch
|
|
101
144
|
* that supplied no sequence of its own, the alignment — so it is reported by
|
|
@@ -28,9 +28,8 @@ import {
|
|
|
28
28
|
import type { MafRegion, MsaViewInitState } from './types'
|
|
29
29
|
import type {
|
|
30
30
|
BlastDatabase,
|
|
31
|
-
BlastProgram,
|
|
32
31
|
MsaAlgorithm,
|
|
33
|
-
} from '../LaunchMsaView/components/
|
|
32
|
+
} from '../LaunchMsaView/components/BlastQuery/consts'
|
|
34
33
|
import type { Feature } from '@jbrowse/core/util'
|
|
35
34
|
import type { Instance } from '@jbrowse/mobx-state-tree'
|
|
36
35
|
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
|
|
@@ -46,13 +45,10 @@ export interface IRegion {
|
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
export interface BlastParams {
|
|
49
|
-
baseUrl: string
|
|
50
48
|
blastDatabase: BlastDatabase
|
|
51
49
|
msaAlgorithm: MsaAlgorithm
|
|
52
|
-
blastProgram: BlastProgram
|
|
53
50
|
selectedTranscript?: Feature
|
|
54
51
|
proteinSequence: string
|
|
55
|
-
rid?: string
|
|
56
52
|
}
|
|
57
53
|
|
|
58
54
|
export interface OrthologParams {
|
|
@@ -60,10 +56,17 @@ export interface OrthologParams {
|
|
|
60
56
|
taxId: number
|
|
61
57
|
/**
|
|
62
58
|
* taxon ids to include as rows (the query taxon is represented by QUERY).
|
|
63
|
-
* Omitted means every species
|
|
64
|
-
* launch that just wants "this gene across species" wants.
|
|
59
|
+
* Omitted means every species NCBI has an ortholog for, in its report order,
|
|
60
|
+
* which is what a launch that just wants "this gene across species" wants.
|
|
65
61
|
*/
|
|
66
62
|
taxa?: number[]
|
|
63
|
+
/**
|
|
64
|
+
* how many ortholog rows to align, `defaultMaxSpecies` when omitted. The
|
|
65
|
+
* aligner is what this bounds: EBI runs at roughly half a second per row for
|
|
66
|
+
* a ~1400aa protein, so a gene with 865 orthologs is a 7 minute job at no
|
|
67
|
+
* cap.
|
|
68
|
+
*/
|
|
69
|
+
maxSpecies?: number
|
|
67
70
|
/** candidate gene identifiers off the feature, tried in order */
|
|
68
71
|
geneCandidates: string[]
|
|
69
72
|
msaAlgorithm: MsaAlgorithm
|
package/src/utils/blastCache.ts
CHANGED
|
@@ -2,9 +2,8 @@ import { createDbOpener } from './idb'
|
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
4
|
BlastDatabase,
|
|
5
|
-
BlastProgram,
|
|
6
5
|
MsaAlgorithm,
|
|
7
|
-
} from '../LaunchMsaView/components/
|
|
6
|
+
} from '../LaunchMsaView/components/BlastQuery/consts'
|
|
8
7
|
import type { DBSchema } from 'idb'
|
|
9
8
|
|
|
10
9
|
const DB_NAME = 'jbrowse-msaview-blast-cache'
|
|
@@ -15,7 +14,12 @@ export interface CachedBlastResult {
|
|
|
15
14
|
id: string
|
|
16
15
|
proteinSequence: string
|
|
17
16
|
blastDatabase: BlastDatabase
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Only ever set on rows cached by a version that still queried NCBI, where
|
|
19
|
+
* the choice between blastp and quick-blastp was real. Kept so those rows
|
|
20
|
+
* still display; never written now.
|
|
21
|
+
*/
|
|
22
|
+
blastProgram?: string
|
|
19
23
|
msaAlgorithm: MsaAlgorithm
|
|
20
24
|
msa: string
|
|
21
25
|
tree: string
|
|
@@ -51,7 +55,6 @@ const getDB = createDbOpener<BlastCacheDB>(
|
|
|
51
55
|
function createCacheKey(
|
|
52
56
|
proteinSequence: string,
|
|
53
57
|
blastDatabase: BlastDatabase,
|
|
54
|
-
blastProgram: BlastProgram,
|
|
55
58
|
msaAlgorithm: MsaAlgorithm,
|
|
56
59
|
transcriptId?: string,
|
|
57
60
|
) {
|
|
@@ -59,13 +62,12 @@ function createCacheKey(
|
|
|
59
62
|
// msaAlgorithm is part of the key because the stored msa/tree are produced by
|
|
60
63
|
// it — without it, re-running the same query under a different algorithm
|
|
61
64
|
// overwrites the earlier result and drops it from the history list
|
|
62
|
-
return `${blastDatabase}:${
|
|
65
|
+
return `${blastDatabase}:${msaAlgorithm}${idPart}:${proteinSequence}`
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
export async function saveBlastResult({
|
|
66
69
|
proteinSequence,
|
|
67
70
|
blastDatabase,
|
|
68
|
-
blastProgram,
|
|
69
71
|
msaAlgorithm,
|
|
70
72
|
msa,
|
|
71
73
|
tree,
|
|
@@ -78,7 +80,6 @@ export async function saveBlastResult({
|
|
|
78
80
|
}: {
|
|
79
81
|
proteinSequence: string
|
|
80
82
|
blastDatabase: BlastDatabase
|
|
81
|
-
blastProgram: BlastProgram
|
|
82
83
|
msaAlgorithm: MsaAlgorithm
|
|
83
84
|
msa: string
|
|
84
85
|
tree: string
|
|
@@ -93,7 +94,6 @@ export async function saveBlastResult({
|
|
|
93
94
|
const id = createCacheKey(
|
|
94
95
|
proteinSequence,
|
|
95
96
|
blastDatabase,
|
|
96
|
-
blastProgram,
|
|
97
97
|
msaAlgorithm,
|
|
98
98
|
transcriptId,
|
|
99
99
|
)
|
|
@@ -101,7 +101,6 @@ export async function saveBlastResult({
|
|
|
101
101
|
id,
|
|
102
102
|
proteinSequence,
|
|
103
103
|
blastDatabase,
|
|
104
|
-
blastProgram,
|
|
105
104
|
msaAlgorithm,
|
|
106
105
|
msa,
|
|
107
106
|
tree,
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { fetchEbiResult, submitEbiJob, waitForEbiJob } from './ebiJobDispatcher'
|
|
2
|
+
|
|
3
|
+
import type { BlastHit } from './types'
|
|
4
|
+
import type { BlastDatabase } from '../LaunchMsaView/components/BlastQuery/consts'
|
|
5
|
+
|
|
6
|
+
const TOOL = 'ncbiblast'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The subset of EBI's ncbiblast JSON result this plugin reads. The service
|
|
10
|
+
* returns a great deal more per hit (urls, bit scores, e-values, the match
|
|
11
|
+
* string); only what the MSA rows are built from is typed here.
|
|
12
|
+
*/
|
|
13
|
+
interface EbiBlastJson {
|
|
14
|
+
hits?: {
|
|
15
|
+
hit_acc?: string
|
|
16
|
+
hit_id?: string
|
|
17
|
+
hit_desc?: string
|
|
18
|
+
/** the UniProt fields are absent on hits from non-UniProt databases */
|
|
19
|
+
hit_os?: string
|
|
20
|
+
hit_uni_de?: string
|
|
21
|
+
hit_uni_os?: string
|
|
22
|
+
/** NCBI taxon id, delivered as a string */
|
|
23
|
+
hit_uni_ox?: string
|
|
24
|
+
hit_hsps?: { hsp_hseq?: string }[]
|
|
25
|
+
}[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Map EBI's hit shape onto the normalized one. Exported for testing against a
|
|
30
|
+
* captured response — the field names are the whole risk here, and nothing else
|
|
31
|
+
* in CI would notice if EBI renamed one.
|
|
32
|
+
*/
|
|
33
|
+
export function normalizeEbiBlastHits(result: EbiBlastJson): BlastHit[] {
|
|
34
|
+
return (result.hits ?? []).map(hit => {
|
|
35
|
+
const taxid = Number.parseInt(hit.hit_uni_ox ?? '', 10)
|
|
36
|
+
return {
|
|
37
|
+
description: [
|
|
38
|
+
{
|
|
39
|
+
accession: hit.hit_acc ?? 'unknown',
|
|
40
|
+
id: hit.hit_id ?? hit.hit_acc ?? 'unknown',
|
|
41
|
+
sciname: hit.hit_uni_os ?? hit.hit_os ?? 'unknown',
|
|
42
|
+
taxid: Number.isNaN(taxid) ? undefined : taxid,
|
|
43
|
+
// hit_uni_de is the bare protein name; hit_desc repeats it with the
|
|
44
|
+
// OS=/OX=/GN= suffix that makeId already covers with real columns
|
|
45
|
+
title: hit.hit_uni_de ?? hit.hit_desc,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
hsps: (hit.hit_hsps ?? []).flatMap(hsp =>
|
|
49
|
+
hsp.hsp_hseq ? [{ hseq: hsp.hsp_hseq }] : [],
|
|
50
|
+
),
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Human-facing link to a job, shown while it runs and on error — so it has to
|
|
57
|
+
* be EBI's own results UI, not the REST result endpoint, which does not exist
|
|
58
|
+
* yet at the moment the link is on screen.
|
|
59
|
+
*/
|
|
60
|
+
export function ebiBlastResultUrl(jobId: string) {
|
|
61
|
+
return `https://www.ebi.ac.uk/jdispatcher/sss/${TOOL}/summary?jobId=${jobId}`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function queryEbiBlastFromJobId({
|
|
65
|
+
jobId,
|
|
66
|
+
onProgress,
|
|
67
|
+
}: {
|
|
68
|
+
jobId: string
|
|
69
|
+
onProgress: (arg: string) => void
|
|
70
|
+
}) {
|
|
71
|
+
onProgress(`Checking BLAST status for job: ${jobId}...`)
|
|
72
|
+
await waitForEbiJob({
|
|
73
|
+
tool: TOOL,
|
|
74
|
+
jobId,
|
|
75
|
+
onCountdown: s => {
|
|
76
|
+
onProgress(`Re-checking BLAST status in... ${s}`)
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const hits = normalizeEbiBlastHits(
|
|
81
|
+
JSON.parse(
|
|
82
|
+
await fetchEbiResult({ tool: TOOL, jobId, type: 'json' }),
|
|
83
|
+
) as EbiBlastJson,
|
|
84
|
+
)
|
|
85
|
+
if (hits.length === 0) {
|
|
86
|
+
throw new Error('No hits found')
|
|
87
|
+
}
|
|
88
|
+
return { rid: jobId, hits }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function queryEbiBlast({
|
|
92
|
+
query,
|
|
93
|
+
blastDatabase,
|
|
94
|
+
onProgress,
|
|
95
|
+
onRid,
|
|
96
|
+
}: {
|
|
97
|
+
query: string
|
|
98
|
+
blastDatabase: BlastDatabase
|
|
99
|
+
onProgress: (arg: string) => void
|
|
100
|
+
onRid: (arg: string) => void
|
|
101
|
+
}) {
|
|
102
|
+
onProgress('Submitting to EBI BLAST...')
|
|
103
|
+
const jobId = await submitEbiJob({
|
|
104
|
+
tool: TOOL,
|
|
105
|
+
params: {
|
|
106
|
+
program: 'blastp',
|
|
107
|
+
stype: 'protein',
|
|
108
|
+
database: blastDatabase,
|
|
109
|
+
sequence: query,
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
onRid(jobId)
|
|
113
|
+
return queryEbiBlastFromJobId({ jobId, onProgress })
|
|
114
|
+
}
|