jbrowse-plugin-msaview 3.2.0 → 3.4.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/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 +61 -17
- 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/ManualMSALoader/ManualMSALoader.js +1 -1
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +9 -4
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.d.ts +9 -0
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +20 -0
- 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/MsaViewPanel/afterCreateAutoruns.js +2 -2
- 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 +41 -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 +68 -0
- package/dist/MsaViewPanel/doLaunchBlast.d.ts +1 -1
- package/dist/MsaViewPanel/doLaunchBlast.js +84 -52
- package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +7 -5
- package/dist/MsaViewPanel/doLaunchOrthologs.js +64 -30
- package/dist/MsaViewPanel/doLaunchOrthologs.test.js +106 -1
- package/dist/MsaViewPanel/genomeToMSA.js +4 -2
- package/dist/MsaViewPanel/genomeToMSA.test.js +34 -0
- package/dist/MsaViewPanel/model.d.ts +41 -11
- package/dist/MsaViewPanel/model.js +6 -0
- package/dist/MsaViewPanel/observeProteinHighlights.test.js +11 -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 +15 -3
- package/dist/utils/ebiBlast.d.ts +1 -1
- package/dist/utils/msa.d.ts +12 -0
- package/dist/utils/msa.js +35 -12
- package/dist/utils/msaRows.d.ts +31 -0
- package/dist/utils/msaRows.js +67 -0
- package/dist/utils/pantherOrthologs.d.ts +79 -0
- package/dist/utils/pantherOrthologs.js +262 -0
- package/dist/utils/phmmer.d.ts +53 -0
- package/dist/utils/phmmer.js +118 -0
- 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 +27 -21
- 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 +88 -30
- 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/ManualMSALoader/ManualMSALoader.tsx +1 -1
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +21 -5
- package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +52 -0
- 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/MsaViewPanel/afterCreateAutoruns.ts +2 -2
- package/src/MsaViewPanel/components/ErrorBoundary.tsx +2 -1
- package/src/MsaViewPanel/components/JobLink.tsx +7 -2
- package/src/MsaViewPanel/components/LaunchProgress.tsx +62 -0
- package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +83 -0
- package/src/MsaViewPanel/components/MsaViewPanel.tsx +7 -4
- package/src/MsaViewPanel/doLaunchBlast.ts +127 -69
- package/src/MsaViewPanel/doLaunchOrthologs.test.ts +119 -2
- package/src/MsaViewPanel/doLaunchOrthologs.ts +100 -38
- package/src/MsaViewPanel/genomeToMSA.test.ts +37 -0
- package/src/MsaViewPanel/genomeToMSA.ts +6 -2
- package/src/MsaViewPanel/model.ts +38 -5
- package/src/MsaViewPanel/observeProteinHighlights.test.ts +13 -0
- package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +1 -0
- package/src/MsaViewPanel/util.ts +18 -0
- package/src/utils/blastCache.ts +33 -12
- package/src/utils/ebiBlast.ts +1 -1
- package/src/utils/msa.ts +43 -12
- package/src/utils/msaRows.ts +95 -0
- package/src/utils/pantherOrthologs.ts +399 -0
- package/src/utils/phmmer.ts +174 -0
- 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
|
@@ -25,6 +25,7 @@ describe('genomeToMSA', () => {
|
|
|
25
25
|
|
|
26
26
|
const model = {
|
|
27
27
|
querySeqName: 'hg38.chr1',
|
|
28
|
+
rows: [['hg38.chr1', 'ACGTACGTAC']],
|
|
28
29
|
transcriptToMsaMap: undefined,
|
|
29
30
|
mafRegion: {
|
|
30
31
|
refName: 'chr1',
|
|
@@ -47,6 +48,7 @@ describe('genomeToMSA', () => {
|
|
|
47
48
|
|
|
48
49
|
const model = {
|
|
49
50
|
querySeqName: 'hg38.chr1',
|
|
51
|
+
rows: [['hg38.chr1', 'ACGTACGTAC']],
|
|
50
52
|
transcriptToMsaMap: undefined,
|
|
51
53
|
mafRegion: {
|
|
52
54
|
refName: 'chr1',
|
|
@@ -75,6 +77,7 @@ describe('genomeToMSA', () => {
|
|
|
75
77
|
|
|
76
78
|
const model = {
|
|
77
79
|
querySeqName: 'hg38.chr1',
|
|
80
|
+
rows: [['hg38.chr1', 'ACGTACGTAC']],
|
|
78
81
|
transcriptToMsaMap: undefined,
|
|
79
82
|
mafRegion: {
|
|
80
83
|
refName: 'chr1',
|
|
@@ -107,6 +110,7 @@ describe('genomeToMSA', () => {
|
|
|
107
110
|
|
|
108
111
|
const model = {
|
|
109
112
|
querySeqName: 'hg38.chr1',
|
|
113
|
+
rows: [['hg38.chr1', 'ACGTACGTAC']],
|
|
110
114
|
transcriptToMsaMap: undefined,
|
|
111
115
|
mafRegion: {
|
|
112
116
|
refName: 'chr1',
|
|
@@ -136,6 +140,7 @@ describe('genomeToMSA', () => {
|
|
|
136
140
|
|
|
137
141
|
const model = {
|
|
138
142
|
querySeqName: 'hg38.chr1',
|
|
143
|
+
rows: [['hg38.chr1', 'ACGTACGTAC']],
|
|
139
144
|
transcriptToMsaMap: undefined,
|
|
140
145
|
mafRegion: {
|
|
141
146
|
refName: 'chr1',
|
|
@@ -165,6 +170,7 @@ describe('genomeToMSA', () => {
|
|
|
165
170
|
|
|
166
171
|
const model = {
|
|
167
172
|
querySeqName: 'hg38.chr1',
|
|
173
|
+
rows: [['hg38.chr1', 'ACGTACGTAC']],
|
|
168
174
|
transcriptToMsaMap: undefined,
|
|
169
175
|
mafRegion: {
|
|
170
176
|
refName: 'chr1',
|
|
@@ -193,6 +199,7 @@ describe('genomeToMSA', () => {
|
|
|
193
199
|
|
|
194
200
|
const model = {
|
|
195
201
|
querySeqName: 'hg38.chr1',
|
|
202
|
+
rows: [['hg38.chr1', 'ACGTACGTAC']],
|
|
196
203
|
transcriptToMsaMap: undefined,
|
|
197
204
|
mafRegion: {
|
|
198
205
|
refName: 'chr1',
|
|
@@ -225,6 +232,7 @@ describe('genomeToMSA', () => {
|
|
|
225
232
|
|
|
226
233
|
const model = {
|
|
227
234
|
querySeqName: 'QUERY',
|
|
235
|
+
rows: [['QUERY', 'MKVLTAEEK']],
|
|
228
236
|
transcriptToMsaMap: {
|
|
229
237
|
refName: 'chr1',
|
|
230
238
|
// g2p is keyed by 0-based genome position, the hover coord is 1-based
|
|
@@ -254,6 +262,7 @@ describe('genomeToMSA', () => {
|
|
|
254
262
|
const mockSeqPosToVisibleCol = vi.fn()
|
|
255
263
|
const model = {
|
|
256
264
|
querySeqName: 'QUERY',
|
|
265
|
+
rows: [['QUERY', 'MKVLTAEEK']],
|
|
257
266
|
transcriptToMsaMap: {
|
|
258
267
|
refName: 'chr1',
|
|
259
268
|
g2p: { 1004: 10 },
|
|
@@ -277,6 +286,7 @@ describe('genomeToMSA', () => {
|
|
|
277
286
|
|
|
278
287
|
const model = {
|
|
279
288
|
querySeqName: 'QUERY',
|
|
289
|
+
rows: [['QUERY', 'MKVLTAEEK']],
|
|
280
290
|
transcriptToMsaMap: {
|
|
281
291
|
refName: 'chr1',
|
|
282
292
|
g2p: { 1000: 0 }, // No entry for 1004
|
|
@@ -301,6 +311,7 @@ describe('genomeToMSA', () => {
|
|
|
301
311
|
|
|
302
312
|
const model = {
|
|
303
313
|
querySeqName: 'QUERY',
|
|
314
|
+
rows: [['QUERY', 'MKVLTAEEK']],
|
|
304
315
|
transcriptToMsaMap: undefined,
|
|
305
316
|
mafRegion: undefined,
|
|
306
317
|
connectedView: { initialized: true },
|
|
@@ -310,4 +321,30 @@ describe('genomeToMSA', () => {
|
|
|
310
321
|
const result = genomeToMSA({ model })
|
|
311
322
|
expect(result).toBeUndefined()
|
|
312
323
|
})
|
|
324
|
+
|
|
325
|
+
// seqPosToVisibleCol answers 0 for a row name it does not know, so without a
|
|
326
|
+
// guard an alignment whose query row is missing -- the default 'QUERY' on an
|
|
327
|
+
// uploaded file, or the empty name the manual panel leaves when it matches
|
|
328
|
+
// nothing -- lights column 0 on every genome hover
|
|
329
|
+
test('returns undefined when querySeqName names no row here', () => {
|
|
330
|
+
mockGetSession.mockReturnValue({
|
|
331
|
+
hovered: {
|
|
332
|
+
hoverFeature: {},
|
|
333
|
+
hoverPosition: { coord: 1005, refName: 'chr1' },
|
|
334
|
+
},
|
|
335
|
+
} as any)
|
|
336
|
+
|
|
337
|
+
const seqPosToVisibleCol = vi.fn(() => 0)
|
|
338
|
+
const model = {
|
|
339
|
+
querySeqName: 'QUERY',
|
|
340
|
+
rows: [['some_other_row', 'MKVLTAEEK']],
|
|
341
|
+
transcriptToMsaMap: { refName: 'chr1', g2p: { 1004: 3 } },
|
|
342
|
+
mafRegion: undefined,
|
|
343
|
+
connectedView: { initialized: true },
|
|
344
|
+
seqPosToVisibleCol,
|
|
345
|
+
} as any
|
|
346
|
+
|
|
347
|
+
expect(genomeToMSA({ model })).toBeUndefined()
|
|
348
|
+
expect(seqPosToVisibleCol).not.toHaveBeenCalled()
|
|
349
|
+
})
|
|
313
350
|
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getSession } from '@jbrowse/core/util'
|
|
2
2
|
|
|
3
|
-
import { hasHoverPosition } from './util'
|
|
3
|
+
import { hasHoverPosition, hasQueryRow } from './util'
|
|
4
4
|
|
|
5
5
|
import type { JBrowsePluginMsaViewModel } from './model'
|
|
6
6
|
|
|
@@ -8,7 +8,11 @@ export function genomeToMSA({ model }: { model: JBrowsePluginMsaViewModel }) {
|
|
|
8
8
|
const { hovered } = getSession(model)
|
|
9
9
|
const { querySeqName, transcriptToMsaMap, connectedView, mafRegion } = model
|
|
10
10
|
|
|
11
|
-
if (
|
|
11
|
+
if (
|
|
12
|
+
!connectedView?.initialized ||
|
|
13
|
+
!hasHoverPosition(hovered) ||
|
|
14
|
+
!hasQueryRow(model)
|
|
15
|
+
) {
|
|
12
16
|
return undefined
|
|
13
17
|
}
|
|
14
18
|
|
|
@@ -25,11 +25,12 @@ import {
|
|
|
25
25
|
msaCoordToGenomeRegions,
|
|
26
26
|
} from './msaCoordToGenomeCoord'
|
|
27
27
|
|
|
28
|
-
import type { MafRegion, MsaViewInitState } from './types'
|
|
29
28
|
import type {
|
|
30
29
|
BlastDatabase,
|
|
31
30
|
MsaAlgorithm,
|
|
31
|
+
PhmmerDatabase,
|
|
32
32
|
} from '../LaunchMsaView/components/BlastQuery/consts'
|
|
33
|
+
import type { MafRegion, MsaViewInitState } from './types'
|
|
33
34
|
import type { Feature } from '@jbrowse/core/util'
|
|
34
35
|
import type { Instance } from '@jbrowse/mobx-state-tree'
|
|
35
36
|
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
|
|
@@ -44,16 +45,48 @@ export interface IRegion {
|
|
|
44
45
|
end: number
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
/**
|
|
49
|
+
* A search to run, discriminated by the program that runs it: the two arms
|
|
50
|
+
* differ in which databases they name and in whether an aligner runs at all, so
|
|
51
|
+
* splitting them is what lets doLaunchBlast read the database without asserting
|
|
52
|
+
* whose it is.
|
|
53
|
+
*
|
|
54
|
+
* The field is still `blastDatabase` rather than `database`: it is persisted in
|
|
55
|
+
* session snapshots and in the IndexedDB result cache, so renaming it would
|
|
56
|
+
* orphan every row already written.
|
|
57
|
+
*/
|
|
58
|
+
export type BlastParams = {
|
|
50
59
|
selectedTranscript?: Feature
|
|
51
60
|
proteinSequence: string
|
|
52
|
-
}
|
|
61
|
+
} & (
|
|
62
|
+
| {
|
|
63
|
+
/** absent on params written before phmmer existed, which were all blastp */
|
|
64
|
+
searchProgram?: 'blastp'
|
|
65
|
+
blastDatabase: BlastDatabase
|
|
66
|
+
msaAlgorithm: MsaAlgorithm
|
|
67
|
+
}
|
|
68
|
+
| {
|
|
69
|
+
searchProgram: 'phmmer'
|
|
70
|
+
/** phmmer names its databases its own way: `swissprot`, not `uniprotkb_swissprot` */
|
|
71
|
+
blastDatabase: PhmmerDatabase
|
|
72
|
+
/** phmmer aligns as it searches, so there is no aligner to choose */
|
|
73
|
+
msaAlgorithm?: undefined
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Where the ortholog set comes from. NCBI's sets cover vertebrates and
|
|
79
|
+
* insects; PANTHER's span its 144 reference proteomes, human to yeast to
|
|
80
|
+
* Arabidopsis, so a gene from outside NCBI's scope aligns only through it.
|
|
81
|
+
*/
|
|
82
|
+
export const orthologSources = ['ncbi', 'panther'] as const
|
|
83
|
+
export type OrthologSource = (typeof orthologSources)[number]
|
|
53
84
|
|
|
54
85
|
export interface OrthologParams {
|
|
55
86
|
/** NCBI taxon id of the assembly the query gene came from */
|
|
56
87
|
taxId: number
|
|
88
|
+
/** `ncbi` when omitted, so every launch written before this key keeps its meaning */
|
|
89
|
+
source?: OrthologSource
|
|
57
90
|
/**
|
|
58
91
|
* taxon ids to include as rows. The query taxon has its own row already, so
|
|
59
92
|
* it is excluded from this set whether or not it is named.
|
|
@@ -30,6 +30,7 @@ function makeModel({ highlightColumns }: { highlightColumns?: number[] } = {}) {
|
|
|
30
30
|
const calls: (number[] | undefined)[] = []
|
|
31
31
|
const model = {
|
|
32
32
|
querySeqName: 'query',
|
|
33
|
+
rows: [['query', 'MKVLTAEEK']],
|
|
33
34
|
connectedViewId: CONNECTED,
|
|
34
35
|
// g2p is indexed by genome coord; identity keeps the arithmetic out of the way
|
|
35
36
|
transcriptToMsaMap: {
|
|
@@ -249,6 +250,18 @@ describe('scope and redundant writes', () => {
|
|
|
249
250
|
expect(calls).toEqual([])
|
|
250
251
|
})
|
|
251
252
|
|
|
253
|
+
// seqPosToGlobalCol answers 0 for a row name it does not know, so without a
|
|
254
|
+
// guard a structure hover would light column 0 of whatever row is first
|
|
255
|
+
test('a query row this alignment does not have contributes no column', () => {
|
|
256
|
+
const { model, calls } = makeModel()
|
|
257
|
+
Object.assign(model, { rows: [['some_other_row', 'MKVLTAEEK']] })
|
|
258
|
+
const run = observeProteinHighlights(model)
|
|
259
|
+
|
|
260
|
+
session({ hover: [{ start: 10, end: 12 }] })
|
|
261
|
+
run()
|
|
262
|
+
expect(calls).toEqual([])
|
|
263
|
+
})
|
|
264
|
+
|
|
252
265
|
test('nothing happens until the view is connected and mapped', () => {
|
|
253
266
|
const { calls } = makeModel()
|
|
254
267
|
const bare = {
|
|
@@ -28,6 +28,7 @@ function makeModel() {
|
|
|
28
28
|
const calls: (number | undefined)[] = []
|
|
29
29
|
const model = {
|
|
30
30
|
querySeqName: 'hg38.chr1',
|
|
31
|
+
rows: [['hg38.chr1', 'ACGTACGTAC']],
|
|
31
32
|
transcriptToMsaMap: undefined,
|
|
32
33
|
mafRegion,
|
|
33
34
|
connectedView: { initialized: true, assemblyNames: ['hg38'] },
|
package/src/MsaViewPanel/util.ts
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether `querySeqName` names a row this alignment actually has.
|
|
3
|
+
*
|
|
4
|
+
* react-msaview's `seqPosToGlobalCol` answers 0 for a name it does not know, so
|
|
5
|
+
* without this every genome position maps to the first column and hovering the
|
|
6
|
+
* genome — or a connected structure — lights column 0 of an unrelated row. The
|
|
7
|
+
* name is wrong more often than it looks: it defaults to `QUERY`, which an
|
|
8
|
+
* uploaded alignment has no reason to carry, and the manual panel leaves it
|
|
9
|
+
* empty when it cannot match the protein to a row.
|
|
10
|
+
*
|
|
11
|
+
* The other direction has no such hole: msaCoordToGenomeRegions needs the query
|
|
12
|
+
* row's sequence to map a column at all, so a missing row is already nothing
|
|
13
|
+
* there.
|
|
14
|
+
*/
|
|
15
|
+
export function hasQueryRow(model: { rows: string[][]; querySeqName: string }) {
|
|
16
|
+
return model.rows.some(r => r[0] === model.querySeqName)
|
|
17
|
+
}
|
|
18
|
+
|
|
1
19
|
export function hasHoverPosition(
|
|
2
20
|
hovered: unknown,
|
|
3
21
|
): hovered is { hoverPosition: { coord: number; refName: string } } {
|
package/src/utils/blastCache.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { createDbOpener } from './idb'
|
|
|
3
3
|
import type {
|
|
4
4
|
BlastDatabase,
|
|
5
5
|
MsaAlgorithm,
|
|
6
|
+
PhmmerDatabase,
|
|
7
|
+
SearchProgram,
|
|
6
8
|
} from '../LaunchMsaView/components/BlastQuery/consts'
|
|
7
9
|
import type { DBSchema } from 'idb'
|
|
8
10
|
|
|
@@ -13,14 +15,17 @@ const DB_VERSION = 2
|
|
|
13
15
|
export interface CachedBlastResult {
|
|
14
16
|
id: string
|
|
15
17
|
proteinSequence: string
|
|
16
|
-
blastDatabase: BlastDatabase
|
|
18
|
+
blastDatabase: BlastDatabase | PhmmerDatabase
|
|
17
19
|
/**
|
|
18
20
|
* Only ever set on rows cached by a version that still queried NCBI, where
|
|
19
21
|
* the choice between blastp and quick-blastp was real. Kept so those rows
|
|
20
22
|
* still display; never written now.
|
|
21
23
|
*/
|
|
22
24
|
blastProgram?: string
|
|
23
|
-
|
|
25
|
+
/** absent on rows cached before phmmer existed, which were all blastp */
|
|
26
|
+
searchProgram?: SearchProgram
|
|
27
|
+
/** absent on phmmer rows, which are aligned by the search itself */
|
|
28
|
+
msaAlgorithm?: MsaAlgorithm
|
|
24
29
|
msa: string
|
|
25
30
|
tree: string
|
|
26
31
|
treeMetadata: string
|
|
@@ -52,13 +57,25 @@ const getDB = createDbOpener<BlastCacheDB>(
|
|
|
52
57
|
},
|
|
53
58
|
)
|
|
54
59
|
|
|
55
|
-
function createCacheKey(
|
|
56
|
-
proteinSequence
|
|
57
|
-
blastDatabase
|
|
58
|
-
msaAlgorithm
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
function createCacheKey({
|
|
61
|
+
proteinSequence,
|
|
62
|
+
blastDatabase,
|
|
63
|
+
msaAlgorithm,
|
|
64
|
+
searchProgram,
|
|
65
|
+
transcriptId,
|
|
66
|
+
}: {
|
|
67
|
+
proteinSequence: string
|
|
68
|
+
blastDatabase: BlastDatabase | PhmmerDatabase
|
|
69
|
+
msaAlgorithm?: MsaAlgorithm
|
|
70
|
+
searchProgram?: SearchProgram
|
|
71
|
+
transcriptId?: string
|
|
72
|
+
}) {
|
|
61
73
|
const idPart = transcriptId ? `:${transcriptId}` : ''
|
|
74
|
+
// phmmer keys are prefixed and blastp keys are left exactly as they were, so
|
|
75
|
+
// results cached before phmmer existed still resolve
|
|
76
|
+
if (searchProgram === 'phmmer') {
|
|
77
|
+
return `phmmer:${blastDatabase}${idPart}:${proteinSequence}`
|
|
78
|
+
}
|
|
62
79
|
// msaAlgorithm is part of the key because the stored msa/tree are produced by
|
|
63
80
|
// it — without it, re-running the same query under a different algorithm
|
|
64
81
|
// overwrites the earlier result and drops it from the history list
|
|
@@ -69,6 +86,7 @@ export async function saveBlastResult({
|
|
|
69
86
|
proteinSequence,
|
|
70
87
|
blastDatabase,
|
|
71
88
|
msaAlgorithm,
|
|
89
|
+
searchProgram,
|
|
72
90
|
msa,
|
|
73
91
|
tree,
|
|
74
92
|
treeMetadata,
|
|
@@ -79,8 +97,9 @@ export async function saveBlastResult({
|
|
|
79
97
|
geneName,
|
|
80
98
|
}: {
|
|
81
99
|
proteinSequence: string
|
|
82
|
-
blastDatabase: BlastDatabase
|
|
83
|
-
msaAlgorithm
|
|
100
|
+
blastDatabase: BlastDatabase | PhmmerDatabase
|
|
101
|
+
msaAlgorithm?: MsaAlgorithm
|
|
102
|
+
searchProgram?: SearchProgram
|
|
84
103
|
msa: string
|
|
85
104
|
tree: string
|
|
86
105
|
treeMetadata: string
|
|
@@ -91,17 +110,19 @@ export async function saveBlastResult({
|
|
|
91
110
|
geneName?: string
|
|
92
111
|
}) {
|
|
93
112
|
const db = await getDB()
|
|
94
|
-
const id = createCacheKey(
|
|
113
|
+
const id = createCacheKey({
|
|
95
114
|
proteinSequence,
|
|
96
115
|
blastDatabase,
|
|
97
116
|
msaAlgorithm,
|
|
117
|
+
searchProgram,
|
|
98
118
|
transcriptId,
|
|
99
|
-
)
|
|
119
|
+
})
|
|
100
120
|
const entry: CachedBlastResult = {
|
|
101
121
|
id,
|
|
102
122
|
proteinSequence,
|
|
103
123
|
blastDatabase,
|
|
104
124
|
msaAlgorithm,
|
|
125
|
+
searchProgram,
|
|
105
126
|
msa,
|
|
106
127
|
tree,
|
|
107
128
|
treeMetadata,
|
package/src/utils/ebiBlast.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { fetchEbiResult, submitEbiJob, waitForEbiJob } from './ebiJobDispatcher'
|
|
2
2
|
|
|
3
|
-
import type { BlastHit } from './types'
|
|
4
3
|
import type { BlastDatabase } from '../LaunchMsaView/components/BlastQuery/consts'
|
|
4
|
+
import type { BlastHit } from './types'
|
|
5
5
|
|
|
6
6
|
const TOOL = 'ncbiblast'
|
|
7
7
|
|
package/src/utils/msa.ts
CHANGED
|
@@ -32,6 +32,43 @@ const algorithms: Record<
|
|
|
32
32
|
},
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Build a tree from an alignment that already exists, which is what the phmmer
|
|
37
|
+
* path needs: phmmer produces the alignment itself, so there is no aligner run
|
|
38
|
+
* to take a guide tree from — and a guide tree is a byproduct of deciding
|
|
39
|
+
* progressive alignment order, not a phylogeny, so it is not what we would want
|
|
40
|
+
* even if there were one. simple_phylogeny is clustalw2's neighbour-joining on
|
|
41
|
+
* a real distance matrix, Kimura-corrected for protein distances.
|
|
42
|
+
*/
|
|
43
|
+
export async function launchTree({
|
|
44
|
+
alignment,
|
|
45
|
+
onProgress,
|
|
46
|
+
}: {
|
|
47
|
+
alignment: string
|
|
48
|
+
onProgress: (arg: string) => void
|
|
49
|
+
}) {
|
|
50
|
+
const tool = 'simple_phylogeny'
|
|
51
|
+
onProgress('Building tree...')
|
|
52
|
+
|
|
53
|
+
const jobId = await submitEbiJob({
|
|
54
|
+
tool,
|
|
55
|
+
params: {
|
|
56
|
+
sequence: alignment,
|
|
57
|
+
tree: 'phylip',
|
|
58
|
+
clustering: 'Neighbour-joining',
|
|
59
|
+
kimura: 'true',
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
await waitForEbiJob({
|
|
63
|
+
tool,
|
|
64
|
+
jobId,
|
|
65
|
+
onCountdown: s => {
|
|
66
|
+
onProgress(`Re-checking tree status in... ${s}`)
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
return fetchEbiResult({ tool, jobId, type: 'tree' })
|
|
70
|
+
}
|
|
71
|
+
|
|
35
72
|
export async function launchMSA({
|
|
36
73
|
algorithm,
|
|
37
74
|
sequence,
|
|
@@ -56,16 +93,10 @@ export async function launchMSA({
|
|
|
56
93
|
onProgress(`Re-checking MSA status in... ${s}`)
|
|
57
94
|
},
|
|
58
95
|
})
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
tree: await fetchEbiResult({
|
|
66
|
-
tool: algorithm,
|
|
67
|
-
jobId,
|
|
68
|
-
type: config.treeResult,
|
|
69
|
-
}),
|
|
70
|
-
}
|
|
96
|
+
// one finished job, two result files, neither derived from the other
|
|
97
|
+
const [msa, tree] = await Promise.all([
|
|
98
|
+
fetchEbiResult({ tool: algorithm, jobId, type: config.msaResult }),
|
|
99
|
+
fetchEbiResult({ tool: algorithm, jobId, type: config.treeResult }),
|
|
100
|
+
])
|
|
101
|
+
return { msa, tree }
|
|
71
102
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { makeId } from '../LaunchMsaView/components/util'
|
|
2
|
+
|
|
3
|
+
import type { PhmmerRow } from './phmmer'
|
|
4
|
+
import type { TaxonomyInfo } from './taxonomyNames'
|
|
5
|
+
import type { BlastHitDescription } from './types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Turning search results into the rows the view is given, kept free of any
|
|
9
|
+
* jbrowse or network import so the whole assembly can be run and checked
|
|
10
|
+
* outside a browser — see test/phmmerLive.test.ts.
|
|
11
|
+
*/
|
|
12
|
+
export function buildRowMetadata(
|
|
13
|
+
desc: BlastHitDescription,
|
|
14
|
+
taxonomyInfo: Map<number, TaxonomyInfo>,
|
|
15
|
+
) {
|
|
16
|
+
const metadata: Record<string, string> = {}
|
|
17
|
+
const taxInfo = desc.taxid ? taxonomyInfo.get(desc.taxid) : undefined
|
|
18
|
+
|
|
19
|
+
if (taxInfo?.sciname) {
|
|
20
|
+
metadata['Scientific name'] = taxInfo.sciname
|
|
21
|
+
}
|
|
22
|
+
if (taxInfo?.commonName) {
|
|
23
|
+
metadata['Common name'] = taxInfo.commonName
|
|
24
|
+
}
|
|
25
|
+
if (desc.accession) {
|
|
26
|
+
metadata.Accession = desc.accession
|
|
27
|
+
}
|
|
28
|
+
if (desc.id) {
|
|
29
|
+
metadata.ID = desc.id
|
|
30
|
+
}
|
|
31
|
+
if (desc.title) {
|
|
32
|
+
metadata.Description = desc.title
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return metadata
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* One target can match the query in several places and phmmer emits a row per
|
|
40
|
+
* matched envelope — four for lamprey albumin against human albumin, which has
|
|
41
|
+
* three domains. Those rows share an accession and so would share a name, and
|
|
42
|
+
* duplicate names silently collapse rows in both the MSA and the tree, so the
|
|
43
|
+
* envelope disambiguates them.
|
|
44
|
+
*/
|
|
45
|
+
export function makeRowNames(
|
|
46
|
+
rows: PhmmerRow[],
|
|
47
|
+
taxonomyInfo: Map<number, TaxonomyInfo>,
|
|
48
|
+
) {
|
|
49
|
+
const baseNames = rows.map(row => makeId(row, taxonomyInfo))
|
|
50
|
+
const counts = new Map<string, number>()
|
|
51
|
+
for (const name of baseNames) {
|
|
52
|
+
counts.set(name, (counts.get(name) ?? 0) + 1)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const used = new Set<string>()
|
|
56
|
+
return baseNames.map((base, i) => {
|
|
57
|
+
let name =
|
|
58
|
+
counts.get(base)! > 1 ? `${base}_${rows[i]!.range ?? i + 1}` : base
|
|
59
|
+
while (used.has(name)) {
|
|
60
|
+
name = `${name}_${i + 1}`
|
|
61
|
+
}
|
|
62
|
+
used.add(name)
|
|
63
|
+
return name
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The phmmer alignment as the view receives it: aligned FASTA whose first row
|
|
69
|
+
* is the query, plus the per-row metadata keyed by the same names, which are
|
|
70
|
+
* also what the tree's leaves are labelled with.
|
|
71
|
+
*/
|
|
72
|
+
export function buildPhmmerMsa({
|
|
73
|
+
rows,
|
|
74
|
+
queryRow,
|
|
75
|
+
taxonomyInfo,
|
|
76
|
+
querySeqName = 'QUERY',
|
|
77
|
+
}: {
|
|
78
|
+
rows: PhmmerRow[]
|
|
79
|
+
queryRow: string
|
|
80
|
+
taxonomyInfo: Map<number, TaxonomyInfo>
|
|
81
|
+
querySeqName?: string
|
|
82
|
+
}) {
|
|
83
|
+
const treeMetadata: Record<string, Record<string, string>> = {}
|
|
84
|
+
const rowNames = makeRowNames(rows, taxonomyInfo)
|
|
85
|
+
const sequences = rows.map((row, i) => {
|
|
86
|
+
const rowName = rowNames[i]!
|
|
87
|
+
treeMetadata[rowName] = buildRowMetadata(row, taxonomyInfo)
|
|
88
|
+
return `>${rowName}\n${row.aligned}`
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
msa: [`>${querySeqName}\n${queryRow}`, ...sequences].join('\n'),
|
|
93
|
+
treeMetadata,
|
|
94
|
+
}
|
|
95
|
+
}
|