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
|
@@ -4,12 +4,17 @@ import { Typography } from '@mui/material'
|
|
|
4
4
|
|
|
5
5
|
import ExternalLink from '../../components/ExternalLink'
|
|
6
6
|
import { ebiBlastResultUrl } from '../../utils/ebiBlast'
|
|
7
|
+
import { isPhmmerJobId, phmmerResultUrl } from '../../utils/phmmer'
|
|
7
8
|
|
|
8
9
|
function JobLink({ jobId }: { jobId: string }) {
|
|
10
|
+
// read off the job id rather than the launch params, so a link rebuilt for an
|
|
11
|
+
// old cached job still points at the tool that actually ran it
|
|
12
|
+
const url = isPhmmerJobId(jobId)
|
|
13
|
+
? phmmerResultUrl(jobId)
|
|
14
|
+
: ebiBlastResultUrl(jobId)
|
|
9
15
|
return (
|
|
10
16
|
<Typography>
|
|
11
|
-
Job {jobId} (
|
|
12
|
-
<ExternalLink href={ebiBlastResultUrl(jobId)}>see status</ExternalLink>)
|
|
17
|
+
Job {jobId} (<ExternalLink href={url}>see status</ExternalLink>)
|
|
13
18
|
</Typography>
|
|
14
19
|
)
|
|
15
20
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
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()({
|
|
13
|
+
margin: {
|
|
14
|
+
padding: 20,
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* What a view shows while it is still building its alignment, and what it shows
|
|
20
|
+
* when that fails.
|
|
21
|
+
*
|
|
22
|
+
* Every launch that resolves something leaves its request on the model until it
|
|
23
|
+
* succeeds -- `blastParams`, `orthologParams`, `init` -- so one still being
|
|
24
|
+
* there IS "no alignment yet", and the error a failed launch records is only
|
|
25
|
+
* readable here. This used to key on `blastParams` alone, which left an ortholog
|
|
26
|
+
* launch rendering an empty MSAView for the minutes its alignment takes and, on
|
|
27
|
+
* failure, forever: the error was set and nothing drew it.
|
|
28
|
+
*/
|
|
29
|
+
const LaunchProgress = observer(function LaunchProgress2({
|
|
30
|
+
model,
|
|
31
|
+
}: {
|
|
32
|
+
model: JBrowsePluginMsaViewModel
|
|
33
|
+
}) {
|
|
34
|
+
const { blastParams, orthologParams, progress, rid, error } = model
|
|
35
|
+
const { classes } = useStyles()
|
|
36
|
+
const message = blastParams
|
|
37
|
+
? 'Running EBI BLAST'
|
|
38
|
+
: orthologParams
|
|
39
|
+
? 'Building ortholog alignment'
|
|
40
|
+
: 'Loading alignment'
|
|
41
|
+
return (
|
|
42
|
+
<div className={classes.margin}>
|
|
43
|
+
{error ? (
|
|
44
|
+
<>
|
|
45
|
+
<Typography variant="h5">{message} failed</Typography>
|
|
46
|
+
{/* the job outlives the browser, so its link is worth keeping next to
|
|
47
|
+
the failure -- EBI's own page says more about a job than we can */}
|
|
48
|
+
{rid ? <JobLink jobId={rid} /> : null}
|
|
49
|
+
<ErrorMessage error={error} />
|
|
50
|
+
</>
|
|
51
|
+
) : (
|
|
52
|
+
<>
|
|
53
|
+
<LoadingEllipses message={message} variant="h5" />
|
|
54
|
+
{rid ? <JobLink jobId={rid} /> : null}
|
|
55
|
+
<Typography>{progress || 'Initializing'}</Typography>
|
|
56
|
+
</>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
export default LaunchProgress
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import { cleanup, render, screen } from '@testing-library/react'
|
|
5
|
+
import { afterEach, expect, test, vi } from 'vitest'
|
|
6
|
+
|
|
7
|
+
import MsaViewPanel from './MsaViewPanel'
|
|
8
|
+
|
|
9
|
+
import type { JBrowsePluginMsaViewModel } from '../model'
|
|
10
|
+
|
|
11
|
+
// react-msaview's MSAView is the "nothing is launching" branch and wants a real
|
|
12
|
+
// MST model; a marker is enough to say the panel reached it
|
|
13
|
+
vi.mock('react-msaview', () => ({
|
|
14
|
+
MSAView: () => <div>the alignment</div>,
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
function panel(model: Partial<JBrowsePluginMsaViewModel>) {
|
|
18
|
+
return render(<MsaViewPanel model={model as JBrowsePluginMsaViewModel} />)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
cleanup()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('an alignment with no pending launch draws itself', () => {
|
|
26
|
+
panel({ progress: '' })
|
|
27
|
+
expect(screen.getByText('the alignment')).toBeTruthy()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('a running BLAST shows its progress, not an empty alignment', () => {
|
|
31
|
+
panel({
|
|
32
|
+
blastParams: { proteinSequence: 'MKV' } as never,
|
|
33
|
+
progress: 'Submitting query',
|
|
34
|
+
})
|
|
35
|
+
expect(screen.queryByText('the alignment')).toBeNull()
|
|
36
|
+
expect(screen.getByText(/Running EBI BLAST/)).toBeTruthy()
|
|
37
|
+
expect(screen.getByText('Submitting query')).toBeTruthy()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// the bug this file was written for: an ortholog launch sets orthologParams
|
|
41
|
+
// rather than blastParams, and the panel keyed on blastParams alone -- so it
|
|
42
|
+
// rendered an empty MSAView for the minutes the alignment takes, and drew
|
|
43
|
+
// nothing at all when the launch failed
|
|
44
|
+
test('a running ortholog launch shows its progress', () => {
|
|
45
|
+
panel({
|
|
46
|
+
orthologParams: { taxId: 9606 } as never,
|
|
47
|
+
progress: 'Resolving orthologs',
|
|
48
|
+
})
|
|
49
|
+
expect(screen.queryByText('the alignment')).toBeNull()
|
|
50
|
+
expect(screen.getByText(/Building ortholog alignment/)).toBeTruthy()
|
|
51
|
+
expect(screen.getByText('Resolving orthologs')).toBeTruthy()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('a failed ortholog launch shows why', () => {
|
|
55
|
+
panel({
|
|
56
|
+
orthologParams: { taxId: 9606 } as never,
|
|
57
|
+
progress: '',
|
|
58
|
+
error: new Error('Only 1 ortholog(s) found for this gene'),
|
|
59
|
+
})
|
|
60
|
+
expect(screen.getByText(/Only 1 ortholog\(s\) found/)).toBeTruthy()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('a failed init shows why', () => {
|
|
64
|
+
panel({
|
|
65
|
+
init: { msaName: 'ENST00000288602' },
|
|
66
|
+
progress: '',
|
|
67
|
+
error: new Error('No alignment named ENST00000288602 in msa.fa.gz'),
|
|
68
|
+
})
|
|
69
|
+
expect(screen.queryByText('the alignment')).toBeNull()
|
|
70
|
+
expect(screen.getByText(/No alignment named ENST00000288602/)).toBeTruthy()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('a running job links out to it', () => {
|
|
74
|
+
panel({
|
|
75
|
+
blastParams: { proteinSequence: 'MKV' } as never,
|
|
76
|
+
progress: 'Re-checking BLAST status in... 7',
|
|
77
|
+
rid: 'ncbiblast-R20260826-123456-0001-abc',
|
|
78
|
+
})
|
|
79
|
+
const link = screen.getByRole('link')
|
|
80
|
+
expect(link.getAttribute('href')).toContain(
|
|
81
|
+
'jobId=ncbiblast-R20260826-123456-0001-abc',
|
|
82
|
+
)
|
|
83
|
+
})
|
|
@@ -6,7 +6,7 @@ import { MSAView } from 'react-msaview'
|
|
|
6
6
|
import { makeStyles } from 'tss-react/mui'
|
|
7
7
|
|
|
8
8
|
import { ErrorBoundary } from './ErrorBoundary'
|
|
9
|
-
import
|
|
9
|
+
import LaunchProgress from './LaunchProgress'
|
|
10
10
|
|
|
11
11
|
import type { JBrowsePluginMsaViewModel } from '../model'
|
|
12
12
|
|
|
@@ -22,12 +22,15 @@ const MsaViewPanel = observer(function MsaViewPanel2({
|
|
|
22
22
|
model: JBrowsePluginMsaViewModel
|
|
23
23
|
}) {
|
|
24
24
|
const { classes } = useStyles()
|
|
25
|
-
const { blastParams, loadingStoredData } = model
|
|
25
|
+
const { blastParams, orthologParams, init, loadingStoredData } = model
|
|
26
|
+
// an unresolved launch request means there is no alignment to draw yet, so all
|
|
27
|
+
// three gate the same panel -- see LaunchProgress
|
|
28
|
+
const launching = !!(blastParams ?? orthologParams ?? init)
|
|
26
29
|
return (
|
|
27
30
|
<ErrorBoundary>
|
|
28
31
|
<div>
|
|
29
|
-
{
|
|
30
|
-
<
|
|
32
|
+
{launching ? (
|
|
33
|
+
<LaunchProgress model={model} />
|
|
31
34
|
) : loadingStoredData ? (
|
|
32
35
|
<div className={classes.loadingContainer}>
|
|
33
36
|
<LoadingEllipses message="Loading MSA data" variant="h6" />
|
|
@@ -2,45 +2,112 @@ import { makeId, strip } from '../LaunchMsaView/components/util'
|
|
|
2
2
|
import { cleanProteinSequence } from '../LaunchMsaView/util'
|
|
3
3
|
import { saveBlastResult } from '../utils/blastCache'
|
|
4
4
|
import { queryEbiBlast } from '../utils/ebiBlast'
|
|
5
|
-
import { launchMSA } from '../utils/msa'
|
|
5
|
+
import { launchMSA, launchTree } from '../utils/msa'
|
|
6
|
+
import { buildPhmmerMsa, buildRowMetadata } from '../utils/msaRows'
|
|
7
|
+
import { queryPhmmer } from '../utils/phmmer'
|
|
6
8
|
import { fetchTaxonomyInfo } from '../utils/taxonomyNames'
|
|
7
9
|
|
|
10
|
+
import type {
|
|
11
|
+
BlastDatabase,
|
|
12
|
+
MsaAlgorithm,
|
|
13
|
+
PhmmerDatabase,
|
|
14
|
+
} from '../LaunchMsaView/components/BlastQuery/consts'
|
|
8
15
|
import type { JBrowsePluginMsaViewModel } from './model'
|
|
9
|
-
|
|
10
|
-
|
|
16
|
+
|
|
17
|
+
type TreeMetadata = Record<string, Record<string, string>>
|
|
11
18
|
|
|
12
19
|
export async function doLaunchBlast({
|
|
13
20
|
self,
|
|
14
21
|
}: {
|
|
15
22
|
self: JBrowsePluginMsaViewModel
|
|
16
23
|
}) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
24
|
+
// kept whole rather than destructured: the database's type depends on
|
|
25
|
+
// searchProgram, and pulling the two apart loses the link between them
|
|
26
|
+
const params = self.blastParams!
|
|
27
|
+
const { selectedTranscript } = params
|
|
28
|
+
const cleanedSeq = cleanProteinSequence(params.proteinSequence)
|
|
20
29
|
|
|
21
30
|
const onProgress = (arg: string) => {
|
|
22
31
|
self.setProgress(arg)
|
|
23
32
|
}
|
|
33
|
+
// publish the job id before the first poll so the view can link out while the
|
|
34
|
+
// job is still running
|
|
35
|
+
const onRid = (r: string) => {
|
|
36
|
+
self.setRid(r)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { msa, tree, treeMetadata, rid } =
|
|
40
|
+
params.searchProgram === 'phmmer'
|
|
41
|
+
? await runPhmmer({
|
|
42
|
+
query: cleanedSeq,
|
|
43
|
+
database: params.blastDatabase,
|
|
44
|
+
onProgress,
|
|
45
|
+
onRid,
|
|
46
|
+
})
|
|
47
|
+
: await runBlast({
|
|
48
|
+
query: cleanedSeq,
|
|
49
|
+
blastDatabase: params.blastDatabase,
|
|
50
|
+
msaAlgorithm: params.msaAlgorithm,
|
|
51
|
+
onProgress,
|
|
52
|
+
onRid,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const treeMetadataJson = JSON.stringify(treeMetadata)
|
|
56
|
+
|
|
57
|
+
await saveBlastResult({
|
|
58
|
+
proteinSequence: cleanedSeq,
|
|
59
|
+
blastDatabase: params.blastDatabase,
|
|
60
|
+
msaAlgorithm: params.msaAlgorithm,
|
|
61
|
+
searchProgram: params.searchProgram,
|
|
62
|
+
msa,
|
|
63
|
+
tree,
|
|
64
|
+
treeMetadata: treeMetadataJson,
|
|
65
|
+
rid,
|
|
66
|
+
geneId: selectedTranscript?.get('parentId'),
|
|
67
|
+
transcriptId: selectedTranscript?.id(),
|
|
68
|
+
transcriptName:
|
|
69
|
+
selectedTranscript?.get('name') ?? selectedTranscript?.get('id'),
|
|
70
|
+
geneName:
|
|
71
|
+
selectedTranscript?.get('gene_name') ??
|
|
72
|
+
selectedTranscript?.get('parentId'),
|
|
73
|
+
})
|
|
24
74
|
|
|
75
|
+
return { msa, tree, treeMetadata: treeMetadataJson }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* BLAST returns each hit already aligned to the query, but pairwise and one hit
|
|
80
|
+
* at a time, so the alignments are stripped back off and every hit is realigned
|
|
81
|
+
* together by a dedicated aligner.
|
|
82
|
+
*/
|
|
83
|
+
async function runBlast({
|
|
84
|
+
query,
|
|
85
|
+
blastDatabase,
|
|
86
|
+
msaAlgorithm,
|
|
87
|
+
onProgress,
|
|
88
|
+
onRid,
|
|
89
|
+
}: {
|
|
90
|
+
query: string
|
|
91
|
+
blastDatabase: BlastDatabase
|
|
92
|
+
msaAlgorithm: MsaAlgorithm
|
|
93
|
+
onProgress: (arg: string) => void
|
|
94
|
+
onRid: (arg: string) => void
|
|
95
|
+
}) {
|
|
25
96
|
const { hits, rid } = await queryEbiBlast({
|
|
26
|
-
query
|
|
97
|
+
query,
|
|
27
98
|
blastDatabase,
|
|
28
99
|
onProgress,
|
|
29
|
-
|
|
30
|
-
// the job is still running
|
|
31
|
-
onRid: r => {
|
|
32
|
-
self.setRid(r)
|
|
33
|
-
},
|
|
100
|
+
onRid,
|
|
34
101
|
})
|
|
35
102
|
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const treeMetadata: Record<string, Record<string, string>> = {}
|
|
103
|
+
onProgress('Fetching species taxonomy info...')
|
|
104
|
+
const taxonomyInfo = await fetchTaxonomyInfo(
|
|
105
|
+
hits
|
|
106
|
+
.map(h => h.description[0]?.taxid)
|
|
107
|
+
.filter((t): t is number => t !== undefined),
|
|
108
|
+
)
|
|
43
109
|
|
|
110
|
+
const treeMetadata: TreeMetadata = {}
|
|
44
111
|
const sequences = hits.map(h => {
|
|
45
112
|
const desc = h.description[0] ?? {
|
|
46
113
|
accession: 'unknown',
|
|
@@ -48,66 +115,57 @@ export async function doLaunchBlast({
|
|
|
48
115
|
sciname: 'unknown',
|
|
49
116
|
}
|
|
50
117
|
const rowName = makeId(desc, taxonomyInfo)
|
|
51
|
-
const seq = strip(h.hsps[0]?.hseq ?? '')
|
|
52
|
-
|
|
53
118
|
treeMetadata[rowName] = buildRowMetadata(desc, taxonomyInfo)
|
|
54
|
-
|
|
55
|
-
return `>${rowName}\n${seq}`
|
|
119
|
+
return `>${rowName}\n${strip(h.hsps[0]?.hseq ?? '')}`
|
|
56
120
|
})
|
|
57
121
|
|
|
58
122
|
const result = await launchMSA({
|
|
59
123
|
algorithm: msaAlgorithm,
|
|
60
|
-
sequence: [`>QUERY\n${
|
|
124
|
+
sequence: [`>QUERY\n${query}`, ...sequences].join('\n'),
|
|
61
125
|
onProgress,
|
|
62
126
|
})
|
|
127
|
+
return { ...result, treeMetadata, rid }
|
|
128
|
+
}
|
|
63
129
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
130
|
+
/**
|
|
131
|
+
* phmmer aligns every hit to a profile of the query as it searches, so its own
|
|
132
|
+
* output is the MSA and there is no realignment step — the hits keep the
|
|
133
|
+
* placement HMMER gave them, and the query row is derived from the alignment's
|
|
134
|
+
* match columns rather than being aligned back in afterwards. That leaves no
|
|
135
|
+
* aligner run to take a tree from, so the tree is built from this alignment.
|
|
136
|
+
*/
|
|
137
|
+
async function runPhmmer({
|
|
138
|
+
query,
|
|
139
|
+
database,
|
|
140
|
+
onProgress,
|
|
141
|
+
onRid,
|
|
142
|
+
}: {
|
|
143
|
+
query: string
|
|
144
|
+
database: PhmmerDatabase
|
|
145
|
+
onProgress: (arg: string) => void
|
|
146
|
+
onRid: (arg: string) => void
|
|
147
|
+
}) {
|
|
148
|
+
const { rows, queryRow, rid } = await queryPhmmer({
|
|
149
|
+
query,
|
|
150
|
+
database,
|
|
151
|
+
onProgress,
|
|
152
|
+
onRid,
|
|
81
153
|
})
|
|
82
154
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function buildRowMetadata(
|
|
90
|
-
desc: BlastHitDescription,
|
|
91
|
-
taxonomyInfo: Map<number, TaxonomyInfo>,
|
|
92
|
-
) {
|
|
93
|
-
const metadata: Record<string, string> = {}
|
|
94
|
-
const taxInfo = desc.taxid ? taxonomyInfo.get(desc.taxid) : undefined
|
|
155
|
+
onProgress('Fetching species taxonomy info...')
|
|
156
|
+
const taxonomyInfo = await fetchTaxonomyInfo(
|
|
157
|
+
rows.map(r => r.taxid).filter((t): t is number => t !== undefined),
|
|
158
|
+
)
|
|
95
159
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
metadata.ID = desc.id
|
|
107
|
-
}
|
|
108
|
-
if (desc.title) {
|
|
109
|
-
metadata.Description = desc.title
|
|
160
|
+
const { msa, treeMetadata } = buildPhmmerMsa({
|
|
161
|
+
rows,
|
|
162
|
+
queryRow,
|
|
163
|
+
taxonomyInfo,
|
|
164
|
+
})
|
|
165
|
+
return {
|
|
166
|
+
msa,
|
|
167
|
+
tree: await launchTree({ alignment: msa, onProgress }),
|
|
168
|
+
treeMetadata,
|
|
169
|
+
rid,
|
|
110
170
|
}
|
|
111
|
-
|
|
112
|
-
return metadata
|
|
113
171
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
|
2
2
|
|
|
3
|
-
import { doLaunchOrthologs } from './doLaunchOrthologs'
|
|
4
3
|
import { launchMSA } from '../utils/msa'
|
|
5
4
|
import {
|
|
6
5
|
defaultMaxSpecies,
|
|
@@ -10,9 +9,10 @@ import {
|
|
|
10
9
|
} from '../utils/ncbiOrthologs'
|
|
11
10
|
import { fetchPantherOrthologs } from '../utils/pantherOrthologs'
|
|
12
11
|
import { fetchTaxonomyInfo } from '../utils/taxonomyNames'
|
|
12
|
+
import { doLaunchOrthologs } from './doLaunchOrthologs'
|
|
13
13
|
|
|
14
|
-
import type { JBrowsePluginMsaViewModel } from './model'
|
|
15
14
|
import type { OrthologRow } from '../utils/ncbiOrthologs'
|
|
15
|
+
import type { JBrowsePluginMsaViewModel } from './model'
|
|
16
16
|
|
|
17
17
|
// Every network call is mocked and nothing else is. What is under test is the
|
|
18
18
|
// argument shaping either side of those calls -- which species get asked for,
|
|
@@ -9,8 +9,8 @@ import {
|
|
|
9
9
|
import { fetchPantherOrthologs } from '../utils/pantherOrthologs'
|
|
10
10
|
import { fetchTaxonomyInfo } from '../utils/taxonomyNames'
|
|
11
11
|
|
|
12
|
-
import type { JBrowsePluginMsaViewModel } from './model'
|
|
13
12
|
import type { OrthologRow } from '../utils/ncbiOrthologs'
|
|
13
|
+
import type { JBrowsePluginMsaViewModel } from './model'
|
|
14
14
|
|
|
15
15
|
interface Representative {
|
|
16
16
|
accession: string
|
|
@@ -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,12 +45,34 @@ 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
|
+
)
|
|
53
76
|
|
|
54
77
|
/**
|
|
55
78
|
* Where the ortholog set comes from. NCBI's sets cover vertebrates and
|
|
@@ -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 = {
|