jbrowse-plugin-msaview 3.3.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 +3 -3
- 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.test.js +1 -1
- package/dist/MsaViewPanel/genomeToMSA.js +4 -2
- package/dist/MsaViewPanel/genomeToMSA.test.js +34 -0
- package/dist/MsaViewPanel/model.d.ts +32 -11
- 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/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 +5 -5
- 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 +2 -2
- package/src/MsaViewPanel/doLaunchOrthologs.ts +1 -1
- package/src/MsaViewPanel/genomeToMSA.test.ts +37 -0
- package/src/MsaViewPanel/genomeToMSA.ts +6 -2
- package/src/MsaViewPanel/model.ts +28 -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/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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { StockholmMSA } from 'msa-parsers'
|
|
2
|
+
|
|
3
|
+
import { fetchEbiResult, submitEbiJob, waitForEbiJob } from './ebiJobDispatcher'
|
|
4
|
+
|
|
5
|
+
import type { PhmmerDatabase } from '../LaunchMsaView/components/BlastQuery/consts'
|
|
6
|
+
|
|
7
|
+
const TOOL = 'hmmer3_phmmer'
|
|
8
|
+
|
|
9
|
+
export interface PhmmerRow {
|
|
10
|
+
accession: string
|
|
11
|
+
id: string
|
|
12
|
+
sciname: string
|
|
13
|
+
taxid?: number
|
|
14
|
+
title?: string
|
|
15
|
+
/** the matched envelope on the target, e.g. '503-912', absent if unparseable */
|
|
16
|
+
range?: string
|
|
17
|
+
/** the row as phmmer aligned it, uppercased with '.' inserts turned into '-' */
|
|
18
|
+
aligned: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PhmmerAlignment {
|
|
22
|
+
rows: PhmmerRow[]
|
|
23
|
+
/**
|
|
24
|
+
* the query, placed into the same columns. phmmer does not put the query in
|
|
25
|
+
* its own output, so this is derived — see buildQueryRow.
|
|
26
|
+
*/
|
|
27
|
+
queryRow: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Human-facing link to a job, shown while it runs and on error.
|
|
32
|
+
*
|
|
33
|
+
* The category has to be sss: jdispatcher serves its shell with a 200 for any
|
|
34
|
+
* category, so /pfa/ and /psa/ look fine to a fetch and render "Page Not Found"
|
|
35
|
+
* in a browser.
|
|
36
|
+
*/
|
|
37
|
+
export function phmmerResultUrl(jobId: string) {
|
|
38
|
+
return `https://www.ebi.ac.uk/jdispatcher/sss/${TOOL}/summary?jobId=${jobId}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** EBI job ids are prefixed with the tool that made them */
|
|
42
|
+
export function isPhmmerJobId(jobId: string) {
|
|
43
|
+
return jobId.startsWith(`${TOOL}-`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* phmmer aligns every hit to a profile built from the query, one match state
|
|
48
|
+
* per query residue, and marks those columns 'x' in #=GC RF. So the query's own
|
|
49
|
+
* row is exactly recoverable: walk RF, consume a query residue at each match
|
|
50
|
+
* column, gap everywhere else.
|
|
51
|
+
*
|
|
52
|
+
* This is the one piece of real logic here rather than a library call, and it
|
|
53
|
+
* is checked hard: if the match columns do not account for the query exactly,
|
|
54
|
+
* the columns and the query have drifted apart, and a query row that is off by
|
|
55
|
+
* even one residue would silently mis-map every column to the genome. Throwing
|
|
56
|
+
* is much better than drawing that.
|
|
57
|
+
*/
|
|
58
|
+
function buildQueryRow({ rf, query }: { rf: string; query: string }) {
|
|
59
|
+
let consumed = 0
|
|
60
|
+
const row = Array.from(rf, c =>
|
|
61
|
+
c === 'x' ? (query[consumed++] ?? '-') : '-',
|
|
62
|
+
).join('')
|
|
63
|
+
if (consumed !== query.length) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`phmmer alignment has ${consumed} match columns for a query of ${query.length} residues, so the query row cannot be placed`,
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
return row
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* '[subseq from] Albumin OS=Homo sapiens OX=9606 GN=ALB PE=1 SV=2' is what a
|
|
73
|
+
* UniProt target's #=GS DE looks like. Hits from the non-UniProt databases
|
|
74
|
+
* phmmer also offers (PDB, AlphaFold, MEROPS...) carry no OS=/OX= at all, so
|
|
75
|
+
* every field here is optional.
|
|
76
|
+
*/
|
|
77
|
+
function parseDescription(de: string | undefined) {
|
|
78
|
+
const text = (de ?? '').replace('[subseq from] ', '')
|
|
79
|
+
const sciname = /OS=(.*?)\s+(?:OX|GN|PE|SV)=/.exec(text)?.[1]
|
|
80
|
+
const ox = /OX=(\d+)/.exec(text)?.[1]
|
|
81
|
+
return {
|
|
82
|
+
sciname: sciname ?? 'unknown',
|
|
83
|
+
taxid: ox ? Number.parseInt(ox, 10) : undefined,
|
|
84
|
+
title: text.split(' OS=')[0] || undefined,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Target names look like 'sp|P02768|ALBU_HUMAN/1-609' for UniProt databases and
|
|
90
|
+
* like anything at all for the others, so an unrecognized name becomes its own
|
|
91
|
+
* accession rather than being dropped.
|
|
92
|
+
*/
|
|
93
|
+
function parseName(name: string) {
|
|
94
|
+
const slash = name.lastIndexOf('/')
|
|
95
|
+
const range =
|
|
96
|
+
slash === -1 ? undefined : /^\d+-\d+$/.exec(name.slice(slash + 1))?.[0]
|
|
97
|
+
const bare = range === undefined ? name : name.slice(0, slash)
|
|
98
|
+
const parts = bare.split('|')
|
|
99
|
+
return parts.length === 3
|
|
100
|
+
? { accession: parts[1]!, id: parts[2]!, range }
|
|
101
|
+
: { accession: bare, id: bare, range }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Exported for testing against a captured .sto — the annotation names (RF, the
|
|
106
|
+
* DE line's OS=/OX=) are the whole risk in this mapping, and nothing else in CI
|
|
107
|
+
* would notice if HMMER or EBI changed one.
|
|
108
|
+
*/
|
|
109
|
+
export function parsePhmmerAlignment({
|
|
110
|
+
stockholm,
|
|
111
|
+
query,
|
|
112
|
+
}: {
|
|
113
|
+
stockholm: string
|
|
114
|
+
query: string
|
|
115
|
+
}): PhmmerAlignment {
|
|
116
|
+
const { gc, gs, seqdata, seqname } = new StockholmMSA(stockholm, 0).getMSA()
|
|
117
|
+
const rf = gc.RF
|
|
118
|
+
if (!rf) {
|
|
119
|
+
throw new Error('phmmer alignment has no #=GC RF line')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
queryRow: buildQueryRow({ rf, query }),
|
|
124
|
+
rows: seqname.map(name => ({
|
|
125
|
+
...parseName(name),
|
|
126
|
+
...parseDescription(gs.DE?.[name]?.[0]),
|
|
127
|
+
// insert columns come back lowercase with '.' for gaps; the MSA renderer
|
|
128
|
+
// looks colors up by the literal letter, so lowercase would draw
|
|
129
|
+
// uncolored. The insert columns stay visible as gaps in the query row.
|
|
130
|
+
aligned: (seqdata[name] ?? '').replaceAll('.', '-').toUpperCase(),
|
|
131
|
+
})),
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function queryPhmmer({
|
|
136
|
+
query,
|
|
137
|
+
database,
|
|
138
|
+
onProgress,
|
|
139
|
+
onRid,
|
|
140
|
+
}: {
|
|
141
|
+
query: string
|
|
142
|
+
database: PhmmerDatabase
|
|
143
|
+
onProgress: (arg: string) => void
|
|
144
|
+
onRid: (arg: string) => void
|
|
145
|
+
}) {
|
|
146
|
+
onProgress('Submitting to EBI phmmer...')
|
|
147
|
+
const jobId = await submitEbiJob({
|
|
148
|
+
tool: TOOL,
|
|
149
|
+
params: {
|
|
150
|
+
database,
|
|
151
|
+
sequence: query,
|
|
152
|
+
// the alignment is the whole point of using phmmer here
|
|
153
|
+
alignView: 'true',
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
onRid(jobId)
|
|
157
|
+
|
|
158
|
+
await waitForEbiJob({
|
|
159
|
+
tool: TOOL,
|
|
160
|
+
jobId,
|
|
161
|
+
onCountdown: s => {
|
|
162
|
+
onProgress(`Re-checking phmmer status in... ${s}`)
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
const alignment = parsePhmmerAlignment({
|
|
167
|
+
stockholm: await fetchEbiResult({ tool: TOOL, jobId, type: 'sto' }),
|
|
168
|
+
query,
|
|
169
|
+
})
|
|
170
|
+
if (alignment.rows.length === 0) {
|
|
171
|
+
throw new Error('No hits found')
|
|
172
|
+
}
|
|
173
|
+
return { rid: jobId, ...alignment }
|
|
174
|
+
}
|
|
@@ -51,8 +51,13 @@ export interface TaxonomyInfo {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
export async function fetchTaxonomyInfo(
|
|
54
|
-
|
|
54
|
+
taxidsWithRepeats: number[],
|
|
55
55
|
): Promise<Map<number, TaxonomyInfo>> {
|
|
56
|
+
// callers pass one taxid per alignment row, and a BLAST hit list is several
|
|
57
|
+
// rows per species: 100 albumin hits are maybe 50 taxa, and asking as they
|
|
58
|
+
// came did 100 IndexedDB reads and sent eutils 100 ids for 50 answers. The
|
|
59
|
+
// result is keyed by taxid, so no caller can tell the difference
|
|
60
|
+
const taxids = [...new Set(taxidsWithRepeats)]
|
|
56
61
|
const result = new Map<number, TaxonomyInfo>()
|
|
57
62
|
const uncachedTaxids: number[] = []
|
|
58
63
|
const cachedResults = await getCachedTaxonomies(taxids)
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '3.
|
|
1
|
+
export const version = '3.4.0'
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { ErrorMessage, LoadingEllipses } from '@jbrowse/core/ui';
|
|
3
|
-
import { Typography } from '@mui/material';
|
|
4
|
-
import { observer } from 'mobx-react';
|
|
5
|
-
import { makeStyles } from 'tss-react/mui';
|
|
6
|
-
import JobLink from './JobLink';
|
|
7
|
-
const useStyles = makeStyles()(theme => ({
|
|
8
|
-
margin: {
|
|
9
|
-
padding: 20,
|
|
10
|
-
},
|
|
11
|
-
loading: {
|
|
12
|
-
background: theme.palette.background.paper,
|
|
13
|
-
},
|
|
14
|
-
}));
|
|
15
|
-
const LoadingBLAST = observer(function LoadingBLAST2({ model, }) {
|
|
16
|
-
const { progress, rid, error } = model;
|
|
17
|
-
const { classes } = useStyles();
|
|
18
|
-
return (React.createElement("div", { className: classes.margin },
|
|
19
|
-
React.createElement(LoadingEllipses, { message: "Running EBI BLAST", variant: "h5" }),
|
|
20
|
-
error ? (React.createElement("div", null,
|
|
21
|
-
rid ? React.createElement(JobLink, { jobId: rid }) : null,
|
|
22
|
-
React.createElement(ErrorMessage, { error: error }))) : rid ? (React.createElement("div", { className: classes.loading },
|
|
23
|
-
React.createElement(JobLink, { jobId: rid }),
|
|
24
|
-
React.createElement(Typography, null, progress))) : (React.createElement(Typography, null, progress || 'Initializing BLAST query'))));
|
|
25
|
-
});
|
|
26
|
-
export default LoadingBLAST;
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
|
|
3
|
-
import { ErrorMessage, LoadingEllipses } from '@jbrowse/core/ui'
|
|
4
|
-
import { Typography } from '@mui/material'
|
|
5
|
-
import { observer } from 'mobx-react'
|
|
6
|
-
import { makeStyles } from 'tss-react/mui'
|
|
7
|
-
|
|
8
|
-
import JobLink from './JobLink'
|
|
9
|
-
|
|
10
|
-
import type { JBrowsePluginMsaViewModel } from '../model'
|
|
11
|
-
|
|
12
|
-
const useStyles = makeStyles()(theme => ({
|
|
13
|
-
margin: {
|
|
14
|
-
padding: 20,
|
|
15
|
-
},
|
|
16
|
-
loading: {
|
|
17
|
-
background: theme.palette.background.paper,
|
|
18
|
-
},
|
|
19
|
-
}))
|
|
20
|
-
|
|
21
|
-
const LoadingBLAST = observer(function LoadingBLAST2({
|
|
22
|
-
model,
|
|
23
|
-
}: {
|
|
24
|
-
model: JBrowsePluginMsaViewModel
|
|
25
|
-
}) {
|
|
26
|
-
const { progress, rid, error } = model
|
|
27
|
-
const { classes } = useStyles()
|
|
28
|
-
return (
|
|
29
|
-
<div className={classes.margin}>
|
|
30
|
-
<LoadingEllipses message="Running EBI BLAST" variant="h5" />
|
|
31
|
-
{error ? (
|
|
32
|
-
<div>
|
|
33
|
-
{rid ? <JobLink jobId={rid} /> : null}
|
|
34
|
-
<ErrorMessage error={error} />
|
|
35
|
-
</div>
|
|
36
|
-
) : rid ? (
|
|
37
|
-
<div className={classes.loading}>
|
|
38
|
-
<JobLink jobId={rid} />
|
|
39
|
-
<Typography>{progress}</Typography>
|
|
40
|
-
</div>
|
|
41
|
-
) : (
|
|
42
|
-
<Typography>{progress || 'Initializing BLAST query'}</Typography>
|
|
43
|
-
)}
|
|
44
|
-
</div>
|
|
45
|
-
)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
export default LoadingBLAST
|