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
|
@@ -5,7 +5,6 @@ import { FormControl, FormControlLabel, Radio, RadioGroup } from '@mui/material'
|
|
|
5
5
|
import { observer } from 'mobx-react'
|
|
6
6
|
import { makeStyles } from 'tss-react/mui'
|
|
7
7
|
|
|
8
|
-
import { launchView } from './launchView'
|
|
9
8
|
import TextField2 from '../../../components/TextField2'
|
|
10
9
|
import { useQueryRowName } from '../../useQueryRowName'
|
|
11
10
|
import { getGeneDisplayName, getLinearGenomeView } from '../../util'
|
|
@@ -14,6 +13,7 @@ import QueryRowSelector from '../QueryRowSelector'
|
|
|
14
13
|
import SubmitCancelActions from '../SubmitCancelActions'
|
|
15
14
|
import TranscriptSelector from '../TranscriptSelector'
|
|
16
15
|
import { useTranscriptSelection } from '../useTranscriptSelection'
|
|
16
|
+
import { launchView } from './launchView'
|
|
17
17
|
|
|
18
18
|
import type {
|
|
19
19
|
AbstractTrackModel,
|
|
@@ -4,10 +4,9 @@ import { Typography } from '@mui/material'
|
|
|
4
4
|
import { observer } from 'mobx-react'
|
|
5
5
|
import { makeStyles } from 'tss-react/mui'
|
|
6
6
|
|
|
7
|
-
import QuerySpeciesSelect from './QuerySpeciesSelect'
|
|
8
|
-
import { orthologLaunchView } from './orthologLaunchView'
|
|
9
7
|
import TextField2 from '../../../components/TextField2'
|
|
10
8
|
import { defaultMaxSpecies } from '../../../utils/ncbiOrthologs'
|
|
9
|
+
import { useLocalStorage } from '../../../utils/useLocalStorage'
|
|
11
10
|
import {
|
|
12
11
|
getGeneDisplayName,
|
|
13
12
|
getGeneIdentifiers,
|
|
@@ -19,7 +18,13 @@ import LaunchPanelContent from '../LaunchPanelContent'
|
|
|
19
18
|
import SubmitCancelActions from '../SubmitCancelActions'
|
|
20
19
|
import TranscriptSelector from '../TranscriptSelector'
|
|
21
20
|
import { useTranscriptSelection } from '../useTranscriptSelection'
|
|
21
|
+
import OrthologSourceSelect, {
|
|
22
|
+
ORTHOLOG_SOURCE_STORAGE_KEY,
|
|
23
|
+
} from './OrthologSourceSelect'
|
|
24
|
+
import QuerySpeciesSelect from './QuerySpeciesSelect'
|
|
25
|
+
import { orthologLaunchView } from './orthologLaunchView'
|
|
22
26
|
|
|
27
|
+
import type { OrthologSource } from '../../../MsaViewPanel/model'
|
|
23
28
|
import type { MsaAlgorithm } from '../BlastQuery/consts'
|
|
24
29
|
import type { AbstractTrackModel, Feature } from '@jbrowse/core/util'
|
|
25
30
|
|
|
@@ -42,6 +47,10 @@ const OrthologPanel = observer(function ({
|
|
|
42
47
|
const view = getLinearGenomeView(model)
|
|
43
48
|
const [launchViewError, setLaunchViewError] = useState<unknown>()
|
|
44
49
|
const [taxId, setTaxId] = useState(9606)
|
|
50
|
+
const [source, setSource] = useLocalStorage<OrthologSource>(
|
|
51
|
+
ORTHOLOG_SOURCE_STORAGE_KEY,
|
|
52
|
+
'ncbi',
|
|
53
|
+
)
|
|
45
54
|
const [msaAlgorithm, setMsaAlgorithm] = useState<MsaAlgorithm>('clustalo')
|
|
46
55
|
const [maxSpecies, setMaxSpecies] = useState(String(defaultMaxSpecies))
|
|
47
56
|
|
|
@@ -65,11 +74,17 @@ const OrthologPanel = observer(function ({
|
|
|
65
74
|
instant, and it is now the aligner that costs the wait, about half a
|
|
66
75
|
second per row. */}
|
|
67
76
|
<Typography variant="body2">
|
|
68
|
-
|
|
69
|
-
|
|
77
|
+
Precomputed orthologs, one gene per species, looked up rather than
|
|
78
|
+
searched for. No BLAST job to queue.
|
|
70
79
|
</Typography>
|
|
71
80
|
|
|
72
81
|
<div>
|
|
82
|
+
<OrthologSourceSelect
|
|
83
|
+
className={classes.selectField}
|
|
84
|
+
value={source}
|
|
85
|
+
onChange={setSource}
|
|
86
|
+
/>
|
|
87
|
+
|
|
73
88
|
<QuerySpeciesSelect
|
|
74
89
|
className={classes.selectField}
|
|
75
90
|
value={taxId}
|
|
@@ -93,7 +108,7 @@ const OrthologPanel = observer(function ({
|
|
|
93
108
|
setMaxSpecies(event.target.value)
|
|
94
109
|
}}
|
|
95
110
|
error={!rowCountValid}
|
|
96
|
-
helperText=
|
|
111
|
+
helperText={`the closest N species ${source === 'panther' ? 'PANTHER' : 'NCBI'} has`}
|
|
97
112
|
/>
|
|
98
113
|
</div>
|
|
99
114
|
|
|
@@ -112,6 +127,7 @@ const OrthologPanel = observer(function ({
|
|
|
112
127
|
newViewTitle: `Orthologs - ${getGeneDisplayName(feature)} - ${getTranscriptDisplayName(selectedTranscript)}`,
|
|
113
128
|
orthologParams: {
|
|
114
129
|
taxId,
|
|
130
|
+
source,
|
|
115
131
|
maxSpecies: rowCount,
|
|
116
132
|
geneCandidates,
|
|
117
133
|
msaAlgorithm,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { MenuItem } from '@mui/material'
|
|
4
|
+
|
|
5
|
+
import TextField2 from '../../../components/TextField2'
|
|
6
|
+
|
|
7
|
+
import type { OrthologSource } from '../../../MsaViewPanel/model'
|
|
8
|
+
|
|
9
|
+
export const ORTHOLOG_SOURCE_STORAGE_KEY = 'msaview-ortholog-source'
|
|
10
|
+
|
|
11
|
+
export const orthologSourceLabels: Record<OrthologSource, string> = {
|
|
12
|
+
ncbi: 'NCBI orthologs',
|
|
13
|
+
panther: 'PANTHER',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Which species a source can answer for, in the words a reader picking one
|
|
17
|
+
// needs: NCBI's ortholog sets stop at vertebrates and insects, PANTHER's run
|
|
18
|
+
// from human to yeast and Arabidopsis.
|
|
19
|
+
const hints: Record<OrthologSource, string> = {
|
|
20
|
+
ncbi: 'vertebrates and insects',
|
|
21
|
+
panther: 'also yeast, worm, fly and plants',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function OrthologSourceSelect({
|
|
25
|
+
value,
|
|
26
|
+
onChange,
|
|
27
|
+
className,
|
|
28
|
+
}: {
|
|
29
|
+
value: OrthologSource
|
|
30
|
+
onChange: (val: OrthologSource) => void
|
|
31
|
+
className?: string
|
|
32
|
+
}) {
|
|
33
|
+
return (
|
|
34
|
+
<TextField2
|
|
35
|
+
variant="outlined"
|
|
36
|
+
label="Source"
|
|
37
|
+
className={className}
|
|
38
|
+
select
|
|
39
|
+
value={value}
|
|
40
|
+
helperText={hints[value]}
|
|
41
|
+
onChange={event => {
|
|
42
|
+
onChange(event.target.value as OrthologSource)
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
{(Object.keys(orthologSourceLabels) as OrthologSource[]).map(val => (
|
|
46
|
+
<MenuItem value={val} key={val}>
|
|
47
|
+
{orthologSourceLabels[val]}
|
|
48
|
+
</MenuItem>
|
|
49
|
+
))}
|
|
50
|
+
</TextField2>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -4,8 +4,8 @@ import React from 'react'
|
|
|
4
4
|
import { cleanup, render, screen } from '@testing-library/react'
|
|
5
5
|
import { afterEach, beforeEach, expect, test, vi } from 'vitest'
|
|
6
6
|
|
|
7
|
-
import SubmitCancelActions from './SubmitCancelActions'
|
|
8
7
|
import { LAUNCH_PLACEMENT_KEY } from '../../utils/workspaces'
|
|
8
|
+
import SubmitCancelActions from './SubmitCancelActions'
|
|
9
9
|
|
|
10
10
|
import type { AbstractTrackModel } from '@jbrowse/core/util'
|
|
11
11
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { getSession } from '@jbrowse/core/util'
|
|
2
2
|
|
|
3
|
+
import { useFetch } from '../../utils/useFetch'
|
|
3
4
|
import { getProteinSequenceFromFeature } from './calculateProteinSequence'
|
|
4
5
|
import { fetchSeq } from './fetchSeq'
|
|
5
|
-
import { useFetch } from '../../utils/useFetch'
|
|
6
6
|
|
|
7
7
|
import type { Feature } from '@jbrowse/core/util'
|
|
8
8
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { findQueryRow } from './detectQueryRow'
|
|
4
4
|
|
|
5
5
|
const protein = 'MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD'
|
|
6
6
|
|
|
@@ -18,25 +18,25 @@ MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD
|
|
|
18
18
|
MKWVTFISLLLLFSSAYSRG--RRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPYD
|
|
19
19
|
`
|
|
20
20
|
|
|
21
|
-
describe('
|
|
21
|
+
describe('findQueryRow', () => {
|
|
22
22
|
test('finds the query by sequence when the aligner renamed it', () => {
|
|
23
|
-
expect(
|
|
23
|
+
expect(findQueryRow(clustal, protein).match).toMatchObject({
|
|
24
24
|
name: 'Query_1',
|
|
25
25
|
quality: 'exact',
|
|
26
26
|
})
|
|
27
27
|
})
|
|
28
28
|
|
|
29
29
|
test('ignores gaps in the aligned row', () => {
|
|
30
|
-
expect(
|
|
30
|
+
expect(findQueryRow(fasta, protein).match?.name).toBe('Query_1')
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
test('tolerates the trailing stop codon the translation carries', () => {
|
|
34
|
-
expect(
|
|
34
|
+
expect(findQueryRow(clustal, `${protein}*`).match?.name).toBe('Query_1')
|
|
35
35
|
})
|
|
36
36
|
|
|
37
37
|
test('matches a row that is the query trimmed to the aligned region', () => {
|
|
38
38
|
const trimmed = `>hit_one\nWRONGWRONGWRONGWRONG\n>aligned_query\n${protein.slice(5, 40)}\n`
|
|
39
|
-
expect(
|
|
39
|
+
expect(findQueryRow(trimmed, protein).match).toMatchObject({
|
|
40
40
|
name: 'aligned_query',
|
|
41
41
|
quality: 'partial',
|
|
42
42
|
})
|
|
@@ -51,21 +51,23 @@ MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD
|
|
|
51
51
|
MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD
|
|
52
52
|
`
|
|
53
53
|
expect(
|
|
54
|
-
|
|
54
|
+
findQueryRow(homologsOnly, 'WWWWWWWWWWWWWWWWWWWWWWWWWWWWWW').match,
|
|
55
55
|
).toBeUndefined()
|
|
56
56
|
})
|
|
57
57
|
|
|
58
58
|
test('returns nothing rather than throwing on a half-pasted alignment', () => {
|
|
59
|
-
expect(
|
|
60
|
-
expect(
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
expect(findQueryRow('>partial\nMKWV', protein).match).toBeUndefined()
|
|
60
|
+
expect(
|
|
61
|
+
findQueryRow('not an alignment at all', protein).match,
|
|
62
|
+
).toBeUndefined()
|
|
63
|
+
expect(findQueryRow('', protein).match).toBeUndefined()
|
|
64
|
+
expect(findQueryRow(clustal, '').match).toBeUndefined()
|
|
63
65
|
})
|
|
64
66
|
})
|
|
65
67
|
|
|
66
|
-
describe('
|
|
68
|
+
describe('findQueryRow row names', () => {
|
|
67
69
|
test('lists the rows for the override dropdown', () => {
|
|
68
|
-
expect(
|
|
70
|
+
expect(findQueryRow(clustal, protein).names).toEqual([
|
|
69
71
|
'Query_1',
|
|
70
72
|
'sp|P02769|ALBU',
|
|
71
73
|
'sp|Q5XLE4|OTHE',
|
|
@@ -73,7 +75,7 @@ describe('getMsaRowNames', () => {
|
|
|
73
75
|
})
|
|
74
76
|
|
|
75
77
|
test('is empty rather than throwing while the user is still pasting', () => {
|
|
76
|
-
expect(
|
|
77
|
-
expect(
|
|
78
|
+
expect(findQueryRow('CLUSTAL W', protein).names).toEqual([])
|
|
79
|
+
expect(findQueryRow('', protein).names).toEqual([])
|
|
78
80
|
})
|
|
79
81
|
})
|
|
@@ -65,27 +65,48 @@ const SIMILARITY_FLOOR = 0.9
|
|
|
65
65
|
*/
|
|
66
66
|
const PARTIAL_COVERAGE_FLOOR = 0.5
|
|
67
67
|
|
|
68
|
-
export
|
|
68
|
+
export interface MsaQueryRow {
|
|
69
|
+
/** every row name, in file order, for the picker to offer */
|
|
70
|
+
names: string[]
|
|
71
|
+
/** the row whose residues are the query's, if one of them is */
|
|
72
|
+
match?: QueryRowMatch
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The picker's whole answer for a pasted alignment: its row names, and which of
|
|
77
|
+
* them is the query.
|
|
78
|
+
*
|
|
79
|
+
* One function rather than two because there is one parse. Both answers were
|
|
80
|
+
* wanted on every keystroke in the paste box, and asking separately parsed a
|
|
81
|
+
* few-hundred-row alignment twice per character.
|
|
82
|
+
*/
|
|
83
|
+
export function findQueryRow(
|
|
69
84
|
msaText: string,
|
|
70
85
|
proteinSequence: string,
|
|
71
|
-
):
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return undefined
|
|
86
|
+
): MsaQueryRow {
|
|
87
|
+
if (!msaText.trim()) {
|
|
88
|
+
return { names: [] }
|
|
75
89
|
}
|
|
76
90
|
|
|
77
|
-
let
|
|
78
|
-
let parsed: { getRow: (name: string) => string }
|
|
91
|
+
let parsed
|
|
79
92
|
try {
|
|
80
|
-
|
|
81
|
-
names = msa.getNames()
|
|
82
|
-
parsed = msa
|
|
93
|
+
parsed = parseMSA(msaText)
|
|
83
94
|
} catch {
|
|
84
95
|
// a half-pasted alignment throws here on every keystroke; the caller shows
|
|
85
96
|
// the field rather than an error
|
|
86
|
-
return
|
|
97
|
+
return { names: [] }
|
|
87
98
|
}
|
|
88
99
|
|
|
100
|
+
const names = parsed.getNames()
|
|
101
|
+
const query = normalize(proteinSequence)
|
|
102
|
+
return { names, match: query ? bestMatch(parsed, names, query) : undefined }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function bestMatch(
|
|
106
|
+
parsed: { getRow: (name: string) => string },
|
|
107
|
+
names: string[],
|
|
108
|
+
query: string,
|
|
109
|
+
): QueryRowMatch | undefined {
|
|
89
110
|
const candidates: QueryRowMatch[] = []
|
|
90
111
|
for (const name of names) {
|
|
91
112
|
const row = normalize(getUngappedSequence(parsed.getRow(name)))
|
|
@@ -112,21 +133,11 @@ export function detectQueryRow(
|
|
|
112
133
|
}
|
|
113
134
|
}
|
|
114
135
|
|
|
115
|
-
|
|
136
|
+
// an exact match returns above, so only these two can be here
|
|
137
|
+
const order: MatchQuality[] = ['partial', 'similar']
|
|
116
138
|
return candidates.sort(
|
|
117
139
|
(a, b) =>
|
|
118
140
|
order.indexOf(a.quality) - order.indexOf(b.quality) ||
|
|
119
141
|
b.identity - a.identity,
|
|
120
142
|
)[0]
|
|
121
143
|
}
|
|
122
|
-
|
|
123
|
-
export function getMsaRowNames(msaText: string): string[] {
|
|
124
|
-
if (!msaText.trim()) {
|
|
125
|
-
return []
|
|
126
|
-
}
|
|
127
|
-
try {
|
|
128
|
-
return parseMSA(msaText).getNames()
|
|
129
|
-
} catch {
|
|
130
|
-
return []
|
|
131
|
-
}
|
|
132
|
-
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo, useState } from 'react'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { findQueryRow } from './detectQueryRow'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The MSA row name to launch with, found by sequence rather than typed.
|
|
@@ -15,19 +15,16 @@ export function useQueryRowName(msaText: string, proteinSequence: string) {
|
|
|
15
15
|
|
|
16
16
|
// parsing runs on every keystroke in the paste box otherwise, and an
|
|
17
17
|
// alignment of a few hundred rows is not free
|
|
18
|
-
const {
|
|
19
|
-
() => (
|
|
20
|
-
detected: detectQueryRow(msaText, proteinSequence),
|
|
21
|
-
names: getMsaRowNames(msaText),
|
|
22
|
-
}),
|
|
18
|
+
const { names, match } = useMemo(
|
|
19
|
+
() => findQueryRow(msaText, proteinSequence),
|
|
23
20
|
[msaText, proteinSequence],
|
|
24
21
|
)
|
|
25
22
|
|
|
26
23
|
return {
|
|
27
|
-
detected,
|
|
24
|
+
detected: match,
|
|
28
25
|
names,
|
|
29
|
-
querySeqName: override ??
|
|
26
|
+
querySeqName: override ?? match?.name ?? '',
|
|
30
27
|
setQuerySeqName: setOverride,
|
|
31
|
-
isAutoDetected: override === undefined && !!
|
|
28
|
+
isAutoDetected: override === undefined && !!match,
|
|
32
29
|
}
|
|
33
30
|
}
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
storeMsaData,
|
|
13
13
|
} from './msaDataStore'
|
|
14
14
|
import { getProteinViews } from './structureConnection'
|
|
15
|
-
import { getUniprotIdFromAlphaFoldUrl } from './util'
|
|
15
|
+
import { getUniprotIdFromAlphaFoldUrl, hasQueryRow } from './util'
|
|
16
16
|
|
|
17
17
|
import type { JBrowsePluginMsaViewModel } from './model'
|
|
18
18
|
|
|
@@ -233,7 +233,7 @@ function genomeHighlightsToVisibleColumns(
|
|
|
233
233
|
field: 'hoverGenomeHighlights' | 'clickGenomeHighlights',
|
|
234
234
|
) {
|
|
235
235
|
const { connectedViewId, transcriptToMsaMap, querySeqName } = self
|
|
236
|
-
if (!transcriptToMsaMap) {
|
|
236
|
+
if (!transcriptToMsaMap || !hasQueryRow(self)) {
|
|
237
237
|
return []
|
|
238
238
|
}
|
|
239
239
|
const { g2p } = transcriptToMsaMap
|
|
@@ -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" />
|