jbrowse-plugin-msaview 2.7.2 → 2.7.4
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/MsaToGenomeHighlight.js +6 -5
- package/dist/LaunchMsaView/components/LaunchMsaViewDialog.js +7 -1
- package/dist/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.js +1 -1
- package/dist/LaunchMsaView/components/NCBIBlastQuery/useCachedBlastResults.d.ts +1 -1
- package/dist/LaunchMsaView/components/NCBIBlastQuery/useCachedBlastResults.js +5 -2
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.d.ts +8 -0
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +86 -0
- package/dist/LaunchMsaView/components/OrthologQuery/orthologLaunchView.d.ts +9 -0
- package/dist/LaunchMsaView/components/OrthologQuery/orthologLaunchView.js +13 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +8 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.js +28 -0
- package/dist/MsaViewPanel/doLaunchBlast.js +10 -22
- package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +23 -0
- package/dist/MsaViewPanel/doLaunchOrthologs.js +97 -0
- package/dist/MsaViewPanel/genomeToMSA.js +13 -6
- package/dist/MsaViewPanel/genomeToMSA.test.js +35 -6
- package/dist/MsaViewPanel/model.d.ts +88 -70
- package/dist/MsaViewPanel/model.js +26 -13
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.d.ts +32 -13
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.js +43 -14
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.js +108 -18
- package/dist/MsaViewPanel/msaDataStore.js +8 -17
- package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +8 -6
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +35 -31
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/blastCache.d.ts +1 -2
- package/dist/utils/blastCache.js +9 -22
- package/dist/utils/domainCache.js +6 -15
- package/dist/utils/idb.d.ts +12 -0
- package/dist/utils/idb.js +21 -0
- package/dist/utils/ncbiOrthologs.d.ts +105 -0
- package/dist/utils/ncbiOrthologs.js +211 -0
- package/dist/utils/ncbiOrthologs.test.d.ts +1 -0
- package/dist/utils/ncbiOrthologs.test.js +41 -0
- package/dist/utils/taxonomyNames.js +13 -18
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +6 -8
- package/src/LaunchMsaView/components/LaunchMsaViewDialog.tsx +13 -2
- package/src/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.tsx +1 -1
- package/src/LaunchMsaView/components/NCBIBlastQuery/useCachedBlastResults.ts +4 -2
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +172 -0
- package/src/LaunchMsaView/components/OrthologQuery/orthologLaunchView.ts +28 -0
- package/src/MsaViewPanel/afterCreateAutoruns.ts +27 -0
- package/src/MsaViewPanel/doLaunchBlast.ts +20 -29
- package/src/MsaViewPanel/doLaunchOrthologs.ts +123 -0
- package/src/MsaViewPanel/genomeToMSA.test.ts +38 -6
- package/src/MsaViewPanel/genomeToMSA.ts +14 -6
- package/src/MsaViewPanel/model.ts +41 -12
- package/src/MsaViewPanel/msaCoordToGenomeCoord.test.ts +117 -18
- package/src/MsaViewPanel/msaCoordToGenomeCoord.ts +70 -26
- package/src/MsaViewPanel/msaDataStore.ts +17 -17
- package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +8 -6
- package/src/utils/blastCache.ts +20 -23
- package/src/utils/domainCache.ts +14 -18
- package/src/utils/idb.ts +28 -0
- package/src/utils/ncbiOrthologs.test.ts +56 -0
- package/src/utils/ncbiOrthologs.ts +320 -0
- package/src/utils/taxonomyNames.ts +22 -24
- package/src/version.ts +1 -1
- package/dist/MsaViewPanel/blosum62.d.ts +0 -2
- package/dist/MsaViewPanel/blosum62.js +0 -627
- package/src/MsaViewPanel/blosum62.ts +0 -628
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import React, { useMemo, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { Checkbox, FormControlLabel, MenuItem, Typography } from '@mui/material'
|
|
4
|
+
import { observer } from 'mobx-react'
|
|
5
|
+
import { makeStyles } from 'tss-react/mui'
|
|
6
|
+
|
|
7
|
+
import { orthologLaunchView } from './orthologLaunchView'
|
|
8
|
+
import TextField2 from '../../../components/TextField2'
|
|
9
|
+
import { COMMON_SPECIES } from '../../../utils/ncbiOrthologs'
|
|
10
|
+
import {
|
|
11
|
+
getGeneDisplayName,
|
|
12
|
+
getGeneIdentifiers,
|
|
13
|
+
getLinearGenomeView,
|
|
14
|
+
getTranscriptDisplayName,
|
|
15
|
+
} from '../../util'
|
|
16
|
+
import LaunchPanelContent from '../LaunchPanelContent'
|
|
17
|
+
import MsaAlgorithmSelect from '../NCBIBlastQuery/MsaAlgorithmSelect'
|
|
18
|
+
import SubmitCancelActions from '../SubmitCancelActions'
|
|
19
|
+
import TranscriptSelector from '../TranscriptSelector'
|
|
20
|
+
import { useTranscriptSelection } from '../useTranscriptSelection'
|
|
21
|
+
|
|
22
|
+
import type { MsaAlgorithm } from '../NCBIBlastQuery/consts'
|
|
23
|
+
import type { AbstractTrackModel, Feature } from '@jbrowse/core/util'
|
|
24
|
+
|
|
25
|
+
const useStyles = makeStyles()({
|
|
26
|
+
selectField: {
|
|
27
|
+
width: 180,
|
|
28
|
+
},
|
|
29
|
+
speciesBox: {
|
|
30
|
+
display: 'flex',
|
|
31
|
+
flexWrap: 'wrap',
|
|
32
|
+
maxWidth: 560,
|
|
33
|
+
marginTop: 12,
|
|
34
|
+
},
|
|
35
|
+
species: {
|
|
36
|
+
width: 160,
|
|
37
|
+
},
|
|
38
|
+
infoText: {
|
|
39
|
+
marginTop: 20,
|
|
40
|
+
maxWidth: 620,
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const OrthologPanel = observer(function ({
|
|
45
|
+
handleClose,
|
|
46
|
+
feature,
|
|
47
|
+
model,
|
|
48
|
+
}: {
|
|
49
|
+
model: AbstractTrackModel
|
|
50
|
+
feature: Feature
|
|
51
|
+
handleClose: () => void
|
|
52
|
+
}) {
|
|
53
|
+
const { classes } = useStyles()
|
|
54
|
+
const view = getLinearGenomeView(model)
|
|
55
|
+
const [launchViewError, setLaunchViewError] = useState<unknown>()
|
|
56
|
+
const [taxId, setTaxId] = useState(9606)
|
|
57
|
+
const [msaAlgorithm, setMsaAlgorithm] = useState<MsaAlgorithm>('clustalo')
|
|
58
|
+
const [excluded, setExcluded] = useState<number[]>([])
|
|
59
|
+
|
|
60
|
+
const geneCandidates = useMemo(() => getGeneIdentifiers(feature), [feature])
|
|
61
|
+
const transcriptSelection = useTranscriptSelection({ feature, view })
|
|
62
|
+
const { selectedTranscript, proteinSequence } = transcriptSelection
|
|
63
|
+
const e = transcriptSelection.error ?? launchViewError
|
|
64
|
+
|
|
65
|
+
const taxa = COMMON_SPECIES.map(s => s.taxId).filter(
|
|
66
|
+
t => !excluded.includes(t),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<>
|
|
71
|
+
<LaunchPanelContent error={e}>
|
|
72
|
+
<Typography>
|
|
73
|
+
Builds the alignment from NCBI's precomputed orthologs — one gene
|
|
74
|
+
per species — instead of searching. There is no job to wait on: the
|
|
75
|
+
NCBI lookups take about a second, and only the multiple alignment at
|
|
76
|
+
EBI costs real time (~10s), against 10+ minutes for BLAST. Rows come
|
|
77
|
+
out labelled by species rather than by accession, and NCBI's CDD
|
|
78
|
+
domains are overlaid automatically.
|
|
79
|
+
</Typography>
|
|
80
|
+
|
|
81
|
+
<div>
|
|
82
|
+
<TextField2
|
|
83
|
+
variant="outlined"
|
|
84
|
+
label="Query species"
|
|
85
|
+
className={classes.selectField}
|
|
86
|
+
select
|
|
87
|
+
value={taxId}
|
|
88
|
+
onChange={event => {
|
|
89
|
+
setTaxId(Number(event.target.value))
|
|
90
|
+
}}
|
|
91
|
+
helperText="the species this gene is from"
|
|
92
|
+
>
|
|
93
|
+
{COMMON_SPECIES.map(s => (
|
|
94
|
+
<MenuItem value={s.taxId} key={s.taxId}>
|
|
95
|
+
{s.label}
|
|
96
|
+
</MenuItem>
|
|
97
|
+
))}
|
|
98
|
+
</TextField2>
|
|
99
|
+
|
|
100
|
+
<MsaAlgorithmSelect
|
|
101
|
+
className={classes.selectField}
|
|
102
|
+
value={msaAlgorithm}
|
|
103
|
+
onChange={setMsaAlgorithm}
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<Typography variant="subtitle2" style={{ marginTop: 12 }}>
|
|
108
|
+
Species to include (those without an ortholog are skipped)
|
|
109
|
+
</Typography>
|
|
110
|
+
<div className={classes.speciesBox}>
|
|
111
|
+
{COMMON_SPECIES.map(s => (
|
|
112
|
+
<FormControlLabel
|
|
113
|
+
className={classes.species}
|
|
114
|
+
key={s.taxId}
|
|
115
|
+
control={
|
|
116
|
+
<Checkbox
|
|
117
|
+
checked={!excluded.includes(s.taxId)}
|
|
118
|
+
onChange={event => {
|
|
119
|
+
setExcluded(
|
|
120
|
+
event.target.checked
|
|
121
|
+
? excluded.filter(t => t !== s.taxId)
|
|
122
|
+
: [...excluded, s.taxId],
|
|
123
|
+
)
|
|
124
|
+
}}
|
|
125
|
+
/>
|
|
126
|
+
}
|
|
127
|
+
label={s.label}
|
|
128
|
+
/>
|
|
129
|
+
))}
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<TranscriptSelector feature={feature} {...transcriptSelection} />
|
|
133
|
+
|
|
134
|
+
<Typography className={classes.infoText} variant="body2">
|
|
135
|
+
The query row is the transcript selected above, not NCBI's
|
|
136
|
+
representative protein, so the alignment stays linked to the genome
|
|
137
|
+
view at codon resolution.
|
|
138
|
+
</Typography>
|
|
139
|
+
</LaunchPanelContent>
|
|
140
|
+
<SubmitCancelActions
|
|
141
|
+
submitDisabled={!proteinSequence || taxa.length < 2}
|
|
142
|
+
onSubmit={() => {
|
|
143
|
+
try {
|
|
144
|
+
if (selectedTranscript) {
|
|
145
|
+
setLaunchViewError(undefined)
|
|
146
|
+
orthologLaunchView({
|
|
147
|
+
feature: selectedTranscript,
|
|
148
|
+
view,
|
|
149
|
+
newViewTitle: `Orthologs - ${getGeneDisplayName(feature)} - ${getTranscriptDisplayName(selectedTranscript)}`,
|
|
150
|
+
orthologParams: {
|
|
151
|
+
taxId,
|
|
152
|
+
taxa,
|
|
153
|
+
geneCandidates,
|
|
154
|
+
msaAlgorithm,
|
|
155
|
+
selectedTranscript,
|
|
156
|
+
proteinSequence,
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
handleClose()
|
|
160
|
+
}
|
|
161
|
+
} catch (e) {
|
|
162
|
+
console.error(e)
|
|
163
|
+
setLaunchViewError(e)
|
|
164
|
+
}
|
|
165
|
+
}}
|
|
166
|
+
onCancel={handleClose}
|
|
167
|
+
/>
|
|
168
|
+
</>
|
|
169
|
+
)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
export default OrthologPanel
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getSession } from '@jbrowse/core/util'
|
|
2
|
+
|
|
3
|
+
import type { OrthologParams } from '../../../MsaViewPanel/model'
|
|
4
|
+
import type { Feature } from '@jbrowse/core/util'
|
|
5
|
+
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
|
|
6
|
+
|
|
7
|
+
export function orthologLaunchView({
|
|
8
|
+
newViewTitle,
|
|
9
|
+
view,
|
|
10
|
+
feature,
|
|
11
|
+
orthologParams,
|
|
12
|
+
}: {
|
|
13
|
+
newViewTitle: string
|
|
14
|
+
view: LinearGenomeViewModel
|
|
15
|
+
feature: Feature
|
|
16
|
+
orthologParams: OrthologParams
|
|
17
|
+
}) {
|
|
18
|
+
getSession(view).addView('MsaView', {
|
|
19
|
+
type: 'MsaView',
|
|
20
|
+
displayName: newViewTitle,
|
|
21
|
+
connectedViewId: view.id,
|
|
22
|
+
connectedFeature: feature.toJSON(),
|
|
23
|
+
drawNodeBubbles: true,
|
|
24
|
+
colWidth: 10,
|
|
25
|
+
rowHeight: 12,
|
|
26
|
+
orthologParams,
|
|
27
|
+
})
|
|
28
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getSession } from '@jbrowse/core/util'
|
|
2
2
|
|
|
3
3
|
import { doLaunchBlast } from './doLaunchBlast'
|
|
4
|
+
import { doLaunchOrthologs } from './doLaunchOrthologs'
|
|
4
5
|
import { fetchIndexedMsa } from './fetchIndexedMsa'
|
|
5
6
|
import { genomeToMSA } from './genomeToMSA'
|
|
6
7
|
import { loadProteinDomains } from './loadProteinDomains'
|
|
@@ -78,6 +79,32 @@ export function storeDataToIndexedDB(self: JBrowsePluginMsaViewModel) {
|
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Same shape as launchBlastIfNeeded, for the ortholog path: the params ARE the
|
|
84
|
+
* request, and clearing them on success is what marks it done. They are left in
|
|
85
|
+
* place on failure so the error stays attributable to a specific request; the
|
|
86
|
+
* autorun's only tracked read is orthologParams itself, so nothing refires
|
|
87
|
+
* until a new request replaces them.
|
|
88
|
+
*/
|
|
89
|
+
export function launchOrthologsIfNeeded(self: JBrowsePluginMsaViewModel) {
|
|
90
|
+
if (self.orthologParams) {
|
|
91
|
+
void (async () => {
|
|
92
|
+
try {
|
|
93
|
+
self.setProgress('Resolving orthologs')
|
|
94
|
+
self.setError(undefined)
|
|
95
|
+
const data = await doLaunchOrthologs({ self })
|
|
96
|
+
self.setData(data)
|
|
97
|
+
self.setOrthologParams(undefined)
|
|
98
|
+
} catch (e) {
|
|
99
|
+
self.setError(e)
|
|
100
|
+
console.error(e)
|
|
101
|
+
} finally {
|
|
102
|
+
self.setProgress('')
|
|
103
|
+
}
|
|
104
|
+
})()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
81
108
|
export function launchBlastIfNeeded(self: JBrowsePluginMsaViewModel) {
|
|
82
109
|
if (self.blastParams) {
|
|
83
110
|
void (async () => {
|
|
@@ -25,36 +25,29 @@ export async function doLaunchBlast({
|
|
|
25
25
|
} = self.blastParams!
|
|
26
26
|
const cleanedSeq = cleanProteinSequence(proteinSequence)
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
const onProgress = (arg: string) => {
|
|
29
|
+
self.setProgress(arg)
|
|
30
|
+
}
|
|
31
|
+
|
|
30
32
|
if (existingRid) {
|
|
33
|
+
// publish it before the first poll so the view can link out to NCBI while
|
|
34
|
+
// the job is still running
|
|
31
35
|
self.setRid(existingRid)
|
|
32
|
-
const result = await queryBlastFromRid({
|
|
33
|
-
rid: existingRid,
|
|
34
|
-
baseUrl,
|
|
35
|
-
onProgress: arg => {
|
|
36
|
-
self.setProgress(arg)
|
|
37
|
-
},
|
|
38
|
-
})
|
|
39
|
-
hits = result.hits
|
|
40
|
-
rid = result.rid
|
|
41
|
-
} else {
|
|
42
|
-
const result = await queryBlast({
|
|
43
|
-
query: cleanedSeq,
|
|
44
|
-
blastDatabase,
|
|
45
|
-
blastProgram,
|
|
46
|
-
baseUrl,
|
|
47
|
-
onProgress: arg => {
|
|
48
|
-
self.setProgress(arg)
|
|
49
|
-
},
|
|
50
|
-
onRid: r => {
|
|
51
|
-
self.setRid(r)
|
|
52
|
-
},
|
|
53
|
-
})
|
|
54
|
-
hits = result.hits
|
|
55
|
-
rid = result.rid
|
|
56
36
|
}
|
|
57
37
|
|
|
38
|
+
const { hits, rid } = existingRid
|
|
39
|
+
? await queryBlastFromRid({ rid: existingRid, baseUrl, onProgress })
|
|
40
|
+
: await queryBlast({
|
|
41
|
+
query: cleanedSeq,
|
|
42
|
+
blastDatabase,
|
|
43
|
+
blastProgram,
|
|
44
|
+
baseUrl,
|
|
45
|
+
onProgress,
|
|
46
|
+
onRid: r => {
|
|
47
|
+
self.setRid(r)
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
|
|
58
51
|
self.setProgress('Fetching species taxonomy info...')
|
|
59
52
|
const taxids = hits
|
|
60
53
|
.map(h => h.description[0]?.taxid)
|
|
@@ -80,9 +73,7 @@ export async function doLaunchBlast({
|
|
|
80
73
|
const result = await launchMSA({
|
|
81
74
|
algorithm: msaAlgorithm,
|
|
82
75
|
sequence: [`>QUERY\n${cleanedSeq}`, ...sequences].join('\n'),
|
|
83
|
-
onProgress
|
|
84
|
-
self.setProgress(arg)
|
|
85
|
-
},
|
|
76
|
+
onProgress,
|
|
86
77
|
})
|
|
87
78
|
|
|
88
79
|
const treeMetadataJson = JSON.stringify(treeMetadata)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { cleanProteinSequence } from '../LaunchMsaView/util'
|
|
2
|
+
import { launchMSA } from '../utils/msa'
|
|
3
|
+
import {
|
|
4
|
+
fetchOrthologRows,
|
|
5
|
+
fetchProteinForGene,
|
|
6
|
+
resolveGeneId,
|
|
7
|
+
} from '../utils/ncbiOrthologs'
|
|
8
|
+
|
|
9
|
+
import type { JBrowsePluginMsaViewModel } from './model'
|
|
10
|
+
import type { OrthologRow } from '../utils/ncbiOrthologs'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The no-search-job alternative to doLaunchBlast.
|
|
14
|
+
*
|
|
15
|
+
* BLAST spends 10+ minutes answering "what looks like this sequence" and
|
|
16
|
+
* returns a redundant, accession-labelled hit list. This asks NCBI the question
|
|
17
|
+
* the alignment actually wants — "what is this gene's ortholog in each species"
|
|
18
|
+
* — which NCBI has already computed, so the whole NCBI half returns in about a
|
|
19
|
+
* second and only the EBI alignment (~10s) costs real time.
|
|
20
|
+
*
|
|
21
|
+
* The query row is the user's OWN selected transcript, not NCBI's
|
|
22
|
+
* representative protein for the query species, because `connectedFeature`
|
|
23
|
+
* maps genome coordinates through that row — swapping in a different isoform
|
|
24
|
+
* would silently break the genome<->MSA linkage. The query species is therefore
|
|
25
|
+
* excluded from the ortholog set rather than appearing twice.
|
|
26
|
+
*/
|
|
27
|
+
export async function doLaunchOrthologs({
|
|
28
|
+
self,
|
|
29
|
+
}: {
|
|
30
|
+
self: JBrowsePluginMsaViewModel
|
|
31
|
+
}) {
|
|
32
|
+
const { taxId, taxa, geneCandidates, msaAlgorithm, proteinSequence } =
|
|
33
|
+
self.orthologParams!
|
|
34
|
+
const cleanedSeq = cleanProteinSequence(proteinSequence)
|
|
35
|
+
|
|
36
|
+
const onProgress = (arg: string) => {
|
|
37
|
+
self.setProgress(arg)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
onProgress('Resolving gene at NCBI...')
|
|
41
|
+
const resolved = await resolveGeneId(geneCandidates, taxId)
|
|
42
|
+
if (!resolved) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Could not resolve any of ${geneCandidates.join(', ')} to an NCBI gene in taxon ${taxId}. Try the NCBI BLAST tab, which needs no gene identifier.`,
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// the query species is represented by the user's own transcript below
|
|
49
|
+
const wanted = new Set(taxa.filter(t => t !== taxId))
|
|
50
|
+
const rows = await fetchOrthologRows({
|
|
51
|
+
geneId: resolved.geneId,
|
|
52
|
+
taxa: wanted,
|
|
53
|
+
onProgress,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const treeMetadata: Record<string, Record<string, string>> = {
|
|
57
|
+
QUERY: await buildQueryMetadata(self, resolved.geneId, cleanedSeq),
|
|
58
|
+
}
|
|
59
|
+
for (const row of rows) {
|
|
60
|
+
treeMetadata[row.label] = buildRowMetadata(row)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const result = await launchMSA({
|
|
64
|
+
algorithm: msaAlgorithm,
|
|
65
|
+
sequence: [
|
|
66
|
+
`>QUERY\n${cleanedSeq}`,
|
|
67
|
+
...rows.map(r => `>${r.label}\n${r.sequence}`),
|
|
68
|
+
].join('\n'),
|
|
69
|
+
onProgress,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
...result,
|
|
74
|
+
treeMetadata: JSON.stringify(treeMetadata),
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The query row is the user's own translated transcript, so it carries an
|
|
80
|
+
* Accession — which is what drives the automatic CDD overlay
|
|
81
|
+
* (afterCreateAutoruns.autoLoadProteinDomains -> loadProteinDomains) — ONLY
|
|
82
|
+
* when its sequence is byte-identical to the RefSeq protein that accession
|
|
83
|
+
* names. Attaching it unconditionally would put every domain box at an offset
|
|
84
|
+
* whenever the user picked a non-representative isoform, which is a silently
|
|
85
|
+
* wrong figure rather than a missing one.
|
|
86
|
+
*/
|
|
87
|
+
async function buildQueryMetadata(
|
|
88
|
+
self: JBrowsePluginMsaViewModel,
|
|
89
|
+
geneId: string,
|
|
90
|
+
proteinSequence: string,
|
|
91
|
+
): Promise<Record<string, string>> {
|
|
92
|
+
const transcript = self.orthologParams?.selectedTranscript
|
|
93
|
+
const metadata: Record<string, string> = { 'Gene ID': geneId }
|
|
94
|
+
const name = transcript?.get('name') ?? transcript?.get('id')
|
|
95
|
+
if (name) {
|
|
96
|
+
metadata.Transcript = name
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const representative = await fetchProteinForGene(geneId)
|
|
100
|
+
if (representative?.sequence === proteinSequence) {
|
|
101
|
+
metadata.Accession = representative.accession
|
|
102
|
+
}
|
|
103
|
+
} catch (e) {
|
|
104
|
+
// a failed lookup only costs the query row its domain overlay, so it must
|
|
105
|
+
// not take down an alignment that is otherwise complete
|
|
106
|
+
console.warn('[msaview-orthologs] query protein lookup failed:', e)
|
|
107
|
+
}
|
|
108
|
+
return metadata
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildRowMetadata(row: OrthologRow): Record<string, string> {
|
|
112
|
+
const metadata: Record<string, string> = {
|
|
113
|
+
'Scientific name': row.scientificName,
|
|
114
|
+
// Accession drives the automatic CDD domain overlay
|
|
115
|
+
// (afterCreateAutoruns.autoLoadProteinDomains -> loadProteinDomains)
|
|
116
|
+
Accession: row.protein,
|
|
117
|
+
'Gene ID': row.geneId,
|
|
118
|
+
}
|
|
119
|
+
if (row.commonName) {
|
|
120
|
+
metadata['Common name'] = row.commonName
|
|
121
|
+
}
|
|
122
|
+
return metadata
|
|
123
|
+
}
|
|
@@ -91,8 +91,9 @@ describe('genomeToMSA', () => {
|
|
|
91
91
|
|
|
92
92
|
const result = genomeToMSA({ model })
|
|
93
93
|
|
|
94
|
-
// coord 1005 -
|
|
95
|
-
|
|
94
|
+
// hover coord 1005 is 1-based, so the 0-based genome position is 1004,
|
|
95
|
+
// which is ungapped position 4 of a region starting at 1000
|
|
96
|
+
expect(mockSeqPosToVisibleCol).toHaveBeenCalledWith('hg38.chr1', 4)
|
|
96
97
|
expect(result).toBe(5)
|
|
97
98
|
})
|
|
98
99
|
|
|
@@ -125,10 +126,11 @@ describe('genomeToMSA', () => {
|
|
|
125
126
|
})
|
|
126
127
|
|
|
127
128
|
test('returns undefined when hover coord is before mafRegion start', () => {
|
|
129
|
+
// 1-based coord 1000 is the 0-based base 999, one before the region
|
|
128
130
|
mockGetSession.mockReturnValue({
|
|
129
131
|
hovered: {
|
|
130
132
|
hoverFeature: {},
|
|
131
|
-
hoverPosition: { coord:
|
|
133
|
+
hoverPosition: { coord: 1000, refName: 'chr1' },
|
|
132
134
|
},
|
|
133
135
|
} as any)
|
|
134
136
|
|
|
@@ -153,10 +155,11 @@ describe('genomeToMSA', () => {
|
|
|
153
155
|
})
|
|
154
156
|
|
|
155
157
|
test('returns undefined when hover coord is at or after mafRegion end', () => {
|
|
158
|
+
// 1-based coord 1011 is the 0-based base 1010, one past the region
|
|
156
159
|
mockGetSession.mockReturnValue({
|
|
157
160
|
hovered: {
|
|
158
161
|
hoverFeature: {},
|
|
159
|
-
hoverPosition: { coord:
|
|
162
|
+
hoverPosition: { coord: 1011, refName: 'chr1' },
|
|
160
163
|
},
|
|
161
164
|
} as any)
|
|
162
165
|
|
|
@@ -223,7 +226,9 @@ describe('genomeToMSA', () => {
|
|
|
223
226
|
const model = {
|
|
224
227
|
querySeqName: 'QUERY',
|
|
225
228
|
transcriptToMsaMap: {
|
|
226
|
-
|
|
229
|
+
refName: 'chr1',
|
|
230
|
+
// g2p is keyed by 0-based genome position, the hover coord is 1-based
|
|
231
|
+
g2p: { 1004: 10 },
|
|
227
232
|
},
|
|
228
233
|
mafRegion: undefined,
|
|
229
234
|
connectedView: { initialized: true },
|
|
@@ -236,6 +241,32 @@ describe('genomeToMSA', () => {
|
|
|
236
241
|
expect(result).toBe(10)
|
|
237
242
|
})
|
|
238
243
|
|
|
244
|
+
test('returns undefined when the hover is on another refName', () => {
|
|
245
|
+
// session.hovered is global, so a hover on an unrelated chromosome can
|
|
246
|
+
// carry a coordinate that happens to be a g2p key
|
|
247
|
+
mockGetSession.mockReturnValue({
|
|
248
|
+
hovered: {
|
|
249
|
+
hoverFeature: {},
|
|
250
|
+
hoverPosition: { coord: 1005, refName: 'chr2' },
|
|
251
|
+
},
|
|
252
|
+
} as any)
|
|
253
|
+
|
|
254
|
+
const mockSeqPosToVisibleCol = vi.fn()
|
|
255
|
+
const model = {
|
|
256
|
+
querySeqName: 'QUERY',
|
|
257
|
+
transcriptToMsaMap: {
|
|
258
|
+
refName: 'chr1',
|
|
259
|
+
g2p: { 1004: 10 },
|
|
260
|
+
},
|
|
261
|
+
mafRegion: undefined,
|
|
262
|
+
connectedView: { initialized: true },
|
|
263
|
+
seqPosToVisibleCol: mockSeqPosToVisibleCol,
|
|
264
|
+
} as any
|
|
265
|
+
|
|
266
|
+
expect(genomeToMSA({ model })).toBeUndefined()
|
|
267
|
+
expect(mockSeqPosToVisibleCol).not.toHaveBeenCalled()
|
|
268
|
+
})
|
|
269
|
+
|
|
239
270
|
test('returns undefined when g2p has no mapping for coord', () => {
|
|
240
271
|
mockGetSession.mockReturnValue({
|
|
241
272
|
hovered: {
|
|
@@ -247,7 +278,8 @@ describe('genomeToMSA', () => {
|
|
|
247
278
|
const model = {
|
|
248
279
|
querySeqName: 'QUERY',
|
|
249
280
|
transcriptToMsaMap: {
|
|
250
|
-
|
|
281
|
+
refName: 'chr1',
|
|
282
|
+
g2p: { 1000: 0 }, // No entry for 1004
|
|
251
283
|
},
|
|
252
284
|
mafRegion: undefined,
|
|
253
285
|
connectedView: { initialized: true },
|
|
@@ -12,22 +12,30 @@ export function genomeToMSA({ model }: { model: JBrowsePluginMsaViewModel }) {
|
|
|
12
12
|
return undefined
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const { coord
|
|
15
|
+
const { coord, refName } = hovered.hoverPosition
|
|
16
|
+
|
|
17
|
+
// hoverPosition.coord is a 1-based display coordinate (core's pxToBp adds the
|
|
18
|
+
// +1), while g2p and mafRegion are keyed by 0-based genome position
|
|
19
|
+
const genomePos = coord - 1
|
|
16
20
|
|
|
17
21
|
if (mafRegion) {
|
|
18
22
|
if (
|
|
19
23
|
refName !== mafRegion.refName ||
|
|
20
24
|
!connectedView.assemblyNames.includes(mafRegion.assemblyName) ||
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
genomePos < mafRegion.start ||
|
|
26
|
+
genomePos >= mafRegion.end
|
|
23
27
|
) {
|
|
24
28
|
return undefined
|
|
25
29
|
}
|
|
26
|
-
return model.seqPosToVisibleCol(querySeqName,
|
|
30
|
+
return model.seqPosToVisibleCol(querySeqName, genomePos - mafRegion.start)
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
// session.hovered is global -- set by whichever LinearGenomeView the cursor
|
|
34
|
+
// was last over, on any assembly -- so the refName gate is load bearing:
|
|
35
|
+
// without it the same numeric coordinate on an unrelated chromosome matches a
|
|
36
|
+
// g2p key and lights up a column for a different locus
|
|
37
|
+
if (refName === transcriptToMsaMap?.refName) {
|
|
38
|
+
const seqPos = transcriptToMsaMap.g2p[genomePos]
|
|
31
39
|
if (seqPos !== undefined) {
|
|
32
40
|
return model.seqPosToVisibleCol(querySeqName, seqPos)
|
|
33
41
|
}
|
|
@@ -12,6 +12,7 @@ export type { MSAFormat } from 'msa-parsers'
|
|
|
12
12
|
import {
|
|
13
13
|
autoLoadProteinDomains,
|
|
14
14
|
launchBlastIfNeeded,
|
|
15
|
+
launchOrthologsIfNeeded,
|
|
15
16
|
loadStoredData,
|
|
16
17
|
observeProteinHighlights,
|
|
17
18
|
processInit,
|
|
@@ -19,7 +20,10 @@ import {
|
|
|
19
20
|
storeDataToIndexedDB,
|
|
20
21
|
syncGenomeHoverToMsaColumn,
|
|
21
22
|
} from './afterCreateAutoruns'
|
|
22
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
msaCoordToGenomeCoord,
|
|
25
|
+
msaCoordToGenomeRegions,
|
|
26
|
+
} from './msaCoordToGenomeCoord'
|
|
23
27
|
|
|
24
28
|
import type { MafRegion, MsaViewInitState } from './types'
|
|
25
29
|
import type {
|
|
@@ -51,6 +55,18 @@ export interface BlastParams {
|
|
|
51
55
|
rid?: string
|
|
52
56
|
}
|
|
53
57
|
|
|
58
|
+
export interface OrthologParams {
|
|
59
|
+
/** NCBI taxon id of the assembly the query gene came from */
|
|
60
|
+
taxId: number
|
|
61
|
+
/** taxon ids to include as rows (the query taxon is represented by QUERY) */
|
|
62
|
+
taxa: number[]
|
|
63
|
+
/** candidate gene identifiers off the feature, tried in order */
|
|
64
|
+
geneCandidates: string[]
|
|
65
|
+
msaAlgorithm: MsaAlgorithm
|
|
66
|
+
selectedTranscript?: Feature
|
|
67
|
+
proteinSequence: string
|
|
68
|
+
}
|
|
69
|
+
|
|
54
70
|
/**
|
|
55
71
|
* #stateModel MsaViewPlugin
|
|
56
72
|
* extends
|
|
@@ -74,6 +90,10 @@ export default function stateModelFactory() {
|
|
|
74
90
|
* #property
|
|
75
91
|
*/
|
|
76
92
|
blastParams: types.frozen<BlastParams | undefined>(),
|
|
93
|
+
/**
|
|
94
|
+
* #property
|
|
95
|
+
*/
|
|
96
|
+
orthologParams: types.frozen<OrthologParams | undefined>(),
|
|
77
97
|
/**
|
|
78
98
|
* #property
|
|
79
99
|
*/
|
|
@@ -188,35 +208,37 @@ export default function stateModelFactory() {
|
|
|
188
208
|
.views(self => ({
|
|
189
209
|
/**
|
|
190
210
|
* #getter
|
|
191
|
-
* Genome
|
|
211
|
+
* Genome regions under the current MSA hover column. Suppressed on the LGV
|
|
192
212
|
* while it's being hovered (GenomeMouseoverHighlight shows the crisp 1bp
|
|
193
213
|
* marker there instead of this wider codon band).
|
|
194
214
|
*/
|
|
195
|
-
get
|
|
215
|
+
get connectedHoverHighlights(): IRegion[] {
|
|
196
216
|
const { mouseCol } = self
|
|
197
217
|
return mouseCol === undefined
|
|
198
|
-
?
|
|
199
|
-
:
|
|
218
|
+
? []
|
|
219
|
+
: msaCoordToGenomeRegions({ model: self, coord: mouseCol })
|
|
200
220
|
},
|
|
201
221
|
/**
|
|
202
222
|
* #getter
|
|
203
|
-
* Genome
|
|
223
|
+
* Genome regions under the persistent MSA click selection. Shown
|
|
204
224
|
* regardless of LGV hover, so hovering the genome doesn't hide it.
|
|
205
225
|
*/
|
|
206
|
-
get
|
|
226
|
+
get connectedClickHighlights(): IRegion[] {
|
|
207
227
|
const { mouseClickCol } = self
|
|
208
228
|
return mouseClickCol === undefined
|
|
209
|
-
?
|
|
210
|
-
:
|
|
229
|
+
? []
|
|
230
|
+
: msaCoordToGenomeRegions({ model: self, coord: mouseClickCol })
|
|
211
231
|
},
|
|
212
232
|
/**
|
|
213
233
|
* #getter
|
|
234
|
+
* cross-plugin contract: jbrowse-plugin-mafviewer reads this off the view
|
|
235
|
+
* to draw the same highlights in its own display
|
|
214
236
|
*/
|
|
215
237
|
get connectedHighlights(): IRegion[] {
|
|
216
238
|
return [
|
|
217
|
-
this.
|
|
218
|
-
this.
|
|
219
|
-
]
|
|
239
|
+
...this.connectedHoverHighlights,
|
|
240
|
+
...this.connectedClickHighlights,
|
|
241
|
+
]
|
|
220
242
|
},
|
|
221
243
|
}))
|
|
222
244
|
|
|
@@ -251,6 +273,12 @@ export default function stateModelFactory() {
|
|
|
251
273
|
setBlastParams(args?: BlastParams) {
|
|
252
274
|
self.blastParams = args
|
|
253
275
|
},
|
|
276
|
+
/**
|
|
277
|
+
* #action
|
|
278
|
+
*/
|
|
279
|
+
setOrthologParams(args?: OrthologParams) {
|
|
280
|
+
self.orthologParams = args
|
|
281
|
+
},
|
|
254
282
|
/**
|
|
255
283
|
* #action
|
|
256
284
|
*/
|
|
@@ -361,6 +389,7 @@ export default function stateModelFactory() {
|
|
|
361
389
|
loadStoredData,
|
|
362
390
|
storeDataToIndexedDB,
|
|
363
391
|
launchBlastIfNeeded,
|
|
392
|
+
launchOrthologsIfNeeded,
|
|
364
393
|
processInit,
|
|
365
394
|
autoLoadProteinDomains,
|
|
366
395
|
]) {
|