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.
Files changed (64) hide show
  1. package/dist/AddHighlightModel/MsaToGenomeHighlight.js +6 -5
  2. package/dist/LaunchMsaView/components/LaunchMsaViewDialog.js +7 -1
  3. package/dist/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.js +1 -1
  4. package/dist/LaunchMsaView/components/NCBIBlastQuery/useCachedBlastResults.d.ts +1 -1
  5. package/dist/LaunchMsaView/components/NCBIBlastQuery/useCachedBlastResults.js +5 -2
  6. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.d.ts +8 -0
  7. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +86 -0
  8. package/dist/LaunchMsaView/components/OrthologQuery/orthologLaunchView.d.ts +9 -0
  9. package/dist/LaunchMsaView/components/OrthologQuery/orthologLaunchView.js +13 -0
  10. package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +8 -0
  11. package/dist/MsaViewPanel/afterCreateAutoruns.js +28 -0
  12. package/dist/MsaViewPanel/doLaunchBlast.js +10 -22
  13. package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +23 -0
  14. package/dist/MsaViewPanel/doLaunchOrthologs.js +97 -0
  15. package/dist/MsaViewPanel/genomeToMSA.js +13 -6
  16. package/dist/MsaViewPanel/genomeToMSA.test.js +35 -6
  17. package/dist/MsaViewPanel/model.d.ts +88 -70
  18. package/dist/MsaViewPanel/model.js +26 -13
  19. package/dist/MsaViewPanel/msaCoordToGenomeCoord.d.ts +32 -13
  20. package/dist/MsaViewPanel/msaCoordToGenomeCoord.js +43 -14
  21. package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.js +108 -18
  22. package/dist/MsaViewPanel/msaDataStore.js +8 -17
  23. package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +8 -6
  24. package/dist/jbrowse-plugin-msaview.umd.production.min.js +35 -31
  25. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  26. package/dist/utils/blastCache.d.ts +1 -2
  27. package/dist/utils/blastCache.js +9 -22
  28. package/dist/utils/domainCache.js +6 -15
  29. package/dist/utils/idb.d.ts +12 -0
  30. package/dist/utils/idb.js +21 -0
  31. package/dist/utils/ncbiOrthologs.d.ts +105 -0
  32. package/dist/utils/ncbiOrthologs.js +211 -0
  33. package/dist/utils/ncbiOrthologs.test.d.ts +1 -0
  34. package/dist/utils/ncbiOrthologs.test.js +41 -0
  35. package/dist/utils/taxonomyNames.js +13 -18
  36. package/dist/version.d.ts +1 -1
  37. package/dist/version.js +1 -1
  38. package/package.json +3 -3
  39. package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +6 -8
  40. package/src/LaunchMsaView/components/LaunchMsaViewDialog.tsx +13 -2
  41. package/src/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.tsx +1 -1
  42. package/src/LaunchMsaView/components/NCBIBlastQuery/useCachedBlastResults.ts +4 -2
  43. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +172 -0
  44. package/src/LaunchMsaView/components/OrthologQuery/orthologLaunchView.ts +28 -0
  45. package/src/MsaViewPanel/afterCreateAutoruns.ts +27 -0
  46. package/src/MsaViewPanel/doLaunchBlast.ts +20 -29
  47. package/src/MsaViewPanel/doLaunchOrthologs.ts +123 -0
  48. package/src/MsaViewPanel/genomeToMSA.test.ts +38 -6
  49. package/src/MsaViewPanel/genomeToMSA.ts +14 -6
  50. package/src/MsaViewPanel/model.ts +41 -12
  51. package/src/MsaViewPanel/msaCoordToGenomeCoord.test.ts +117 -18
  52. package/src/MsaViewPanel/msaCoordToGenomeCoord.ts +70 -26
  53. package/src/MsaViewPanel/msaDataStore.ts +17 -17
  54. package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +8 -6
  55. package/src/utils/blastCache.ts +20 -23
  56. package/src/utils/domainCache.ts +14 -18
  57. package/src/utils/idb.ts +28 -0
  58. package/src/utils/ncbiOrthologs.test.ts +56 -0
  59. package/src/utils/ncbiOrthologs.ts +320 -0
  60. package/src/utils/taxonomyNames.ts +22 -24
  61. package/src/version.ts +1 -1
  62. package/dist/MsaViewPanel/blosum62.d.ts +0 -2
  63. package/dist/MsaViewPanel/blosum62.js +0 -627
  64. 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&apos;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&apos;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&apos;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
- let hits
29
- let rid: string
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: arg => {
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 - start 1000 = ungapped position 5
95
- expect(mockSeqPosToVisibleCol).toHaveBeenCalledWith('hg38.chr1', 5)
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: 999, refName: 'chr1' },
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: 1010, refName: 'chr1' },
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
- g2p: { 1005: 10 },
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
- g2p: { 1000: 0 }, // No entry for 1005
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: hoverCoord, refName } = hovered.hoverPosition
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
- hoverCoord < mafRegion.start ||
22
- hoverCoord >= mafRegion.end
25
+ genomePos < mafRegion.start ||
26
+ genomePos >= mafRegion.end
23
27
  ) {
24
28
  return undefined
25
29
  }
26
- return model.seqPosToVisibleCol(querySeqName, hoverCoord - mafRegion.start)
30
+ return model.seqPosToVisibleCol(querySeqName, genomePos - mafRegion.start)
27
31
  }
28
32
 
29
- if (transcriptToMsaMap) {
30
- const seqPos = transcriptToMsaMap.g2p[hoverCoord]
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 { msaCoordToGenomeCoord } from './msaCoordToGenomeCoord'
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 region under the current MSA hover column. Suppressed on the LGV
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 connectedHoverHighlight(): IRegion | undefined {
215
+ get connectedHoverHighlights(): IRegion[] {
196
216
  const { mouseCol } = self
197
217
  return mouseCol === undefined
198
- ? undefined
199
- : msaCoordToGenomeCoord({ model: self, coord: mouseCol })
218
+ ? []
219
+ : msaCoordToGenomeRegions({ model: self, coord: mouseCol })
200
220
  },
201
221
  /**
202
222
  * #getter
203
- * Genome region under the persistent MSA click selection. Shown
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 connectedClickHighlight(): IRegion | undefined {
226
+ get connectedClickHighlights(): IRegion[] {
207
227
  const { mouseClickCol } = self
208
228
  return mouseClickCol === undefined
209
- ? undefined
210
- : msaCoordToGenomeCoord({ model: self, coord: mouseClickCol })
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.connectedHoverHighlight,
218
- this.connectedClickHighlight,
219
- ].filter((r): r is IRegion => r !== undefined)
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
  ]) {