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.
Files changed (97) hide show
  1. package/dist/AddHighlightModel/GenomeMouseoverHighlight.js +1 -1
  2. package/dist/AddHighlightModel/MsaToGenomeHighlight.js +1 -1
  3. package/dist/AddHighlightModel/index.js +1 -1
  4. package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +61 -17
  5. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -1
  6. package/dist/LaunchMsaView/components/BlastQuery/BlastPanel.js +2 -2
  7. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.d.ts +12 -0
  8. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.js +21 -2
  9. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.d.ts +1 -0
  10. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.js +29 -0
  11. package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +1 -1
  12. package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +28 -0
  13. package/dist/LaunchMsaView/components/BlastQuery/consts.js +21 -0
  14. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +1 -1
  15. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +3 -3
  16. package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +1 -1
  17. package/dist/LaunchMsaView/components/useFeatureSequence.js +1 -1
  18. package/dist/LaunchMsaView/detectQueryRow.d.ts +15 -2
  19. package/dist/LaunchMsaView/detectQueryRow.js +20 -21
  20. package/dist/LaunchMsaView/detectQueryRow.test.js +15 -15
  21. package/dist/LaunchMsaView/useQueryRowName.js +5 -8
  22. package/dist/MsaViewPanel/afterCreateAutoruns.js +2 -2
  23. package/dist/MsaViewPanel/components/ErrorBoundary.d.ts +2 -2
  24. package/dist/MsaViewPanel/components/JobLink.js +7 -1
  25. package/dist/MsaViewPanel/components/LaunchProgress.d.ts +17 -0
  26. package/dist/MsaViewPanel/components/LaunchProgress.js +41 -0
  27. package/dist/MsaViewPanel/components/MsaViewPanel.js +6 -3
  28. package/dist/MsaViewPanel/components/MsaViewPanel.test.d.ts +1 -0
  29. package/dist/MsaViewPanel/components/MsaViewPanel.test.js +68 -0
  30. package/dist/MsaViewPanel/doLaunchBlast.d.ts +1 -1
  31. package/dist/MsaViewPanel/doLaunchBlast.js +84 -52
  32. package/dist/MsaViewPanel/doLaunchOrthologs.test.js +1 -1
  33. package/dist/MsaViewPanel/genomeToMSA.js +4 -2
  34. package/dist/MsaViewPanel/genomeToMSA.test.js +34 -0
  35. package/dist/MsaViewPanel/model.d.ts +32 -11
  36. package/dist/MsaViewPanel/observeProteinHighlights.test.js +11 -0
  37. package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +1 -0
  38. package/dist/MsaViewPanel/util.d.ts +18 -0
  39. package/dist/MsaViewPanel/util.js +17 -0
  40. package/dist/jbrowse-plugin-msaview.umd.production.min.js +47 -35
  41. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  42. package/dist/utils/blastCache.d.ts +10 -6
  43. package/dist/utils/blastCache.js +15 -3
  44. package/dist/utils/ebiBlast.d.ts +1 -1
  45. package/dist/utils/msa.d.ts +12 -0
  46. package/dist/utils/msa.js +35 -12
  47. package/dist/utils/msaRows.d.ts +31 -0
  48. package/dist/utils/msaRows.js +67 -0
  49. package/dist/utils/phmmer.d.ts +53 -0
  50. package/dist/utils/phmmer.js +118 -0
  51. package/dist/utils/taxonomyNames.d.ts +1 -1
  52. package/dist/utils/taxonomyNames.js +6 -1
  53. package/dist/version.d.ts +1 -1
  54. package/dist/version.js +1 -1
  55. package/package.json +27 -21
  56. package/src/AddHighlightModel/GenomeMouseoverHighlight.tsx +1 -1
  57. package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +1 -1
  58. package/src/AddHighlightModel/index.tsx +1 -1
  59. package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +88 -30
  60. package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -1
  61. package/src/LaunchMsaView/components/BlastQuery/BlastPanel.tsx +4 -4
  62. package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.ts +50 -0
  63. package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.tsx +23 -3
  64. package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +1 -1
  65. package/src/LaunchMsaView/components/BlastQuery/consts.ts +40 -0
  66. package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -1
  67. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +5 -5
  68. package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +1 -1
  69. package/src/LaunchMsaView/components/useFeatureSequence.ts +1 -1
  70. package/src/LaunchMsaView/detectQueryRow.test.ts +17 -15
  71. package/src/LaunchMsaView/detectQueryRow.ts +34 -23
  72. package/src/LaunchMsaView/useQueryRowName.ts +6 -9
  73. package/src/MsaViewPanel/afterCreateAutoruns.ts +2 -2
  74. package/src/MsaViewPanel/components/ErrorBoundary.tsx +2 -1
  75. package/src/MsaViewPanel/components/JobLink.tsx +7 -2
  76. package/src/MsaViewPanel/components/LaunchProgress.tsx +62 -0
  77. package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +83 -0
  78. package/src/MsaViewPanel/components/MsaViewPanel.tsx +7 -4
  79. package/src/MsaViewPanel/doLaunchBlast.ts +127 -69
  80. package/src/MsaViewPanel/doLaunchOrthologs.test.ts +2 -2
  81. package/src/MsaViewPanel/doLaunchOrthologs.ts +1 -1
  82. package/src/MsaViewPanel/genomeToMSA.test.ts +37 -0
  83. package/src/MsaViewPanel/genomeToMSA.ts +6 -2
  84. package/src/MsaViewPanel/model.ts +28 -5
  85. package/src/MsaViewPanel/observeProteinHighlights.test.ts +13 -0
  86. package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +1 -0
  87. package/src/MsaViewPanel/util.ts +18 -0
  88. package/src/utils/blastCache.ts +33 -12
  89. package/src/utils/ebiBlast.ts +1 -1
  90. package/src/utils/msa.ts +43 -12
  91. package/src/utils/msaRows.ts +95 -0
  92. package/src/utils/phmmer.ts +174 -0
  93. package/src/utils/taxonomyNames.ts +6 -1
  94. package/src/version.ts +1 -1
  95. package/dist/MsaViewPanel/components/LoadingBLAST.d.ts +0 -6
  96. package/dist/MsaViewPanel/components/LoadingBLAST.js +0 -26
  97. 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 LoadingBLAST from './LoadingBLAST'
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
- {blastParams ? (
30
- <LoadingBLAST model={model} />
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
- import type { TaxonomyInfo } from '../utils/taxonomyNames'
10
- import type { BlastHitDescription } from '../utils/types'
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
- const { blastDatabase, msaAlgorithm, proteinSequence, selectedTranscript } =
18
- self.blastParams!
19
- const cleanedSeq = cleanProteinSequence(proteinSequence)
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: cleanedSeq,
97
+ query,
27
98
  blastDatabase,
28
99
  onProgress,
29
- // publish the job id before the first poll so the view can link out while
30
- // the job is still running
31
- onRid: r => {
32
- self.setRid(r)
33
- },
100
+ onRid,
34
101
  })
35
102
 
36
- self.setProgress('Fetching species taxonomy info...')
37
- const taxids = hits
38
- .map(h => h.description[0]?.taxid)
39
- .filter((t): t is number => t !== undefined)
40
- const taxonomyInfo = await fetchTaxonomyInfo(taxids)
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${cleanedSeq}`, ...sequences].join('\n'),
124
+ sequence: [`>QUERY\n${query}`, ...sequences].join('\n'),
61
125
  onProgress,
62
126
  })
127
+ return { ...result, treeMetadata, rid }
128
+ }
63
129
 
64
- const treeMetadataJson = JSON.stringify(treeMetadata)
65
-
66
- await saveBlastResult({
67
- proteinSequence: cleanedSeq,
68
- blastDatabase,
69
- msaAlgorithm,
70
- msa: result.msa,
71
- tree: result.tree,
72
- treeMetadata: treeMetadataJson,
73
- rid,
74
- geneId: selectedTranscript?.get('parentId'),
75
- transcriptId: selectedTranscript?.id(),
76
- transcriptName:
77
- selectedTranscript?.get('name') ?? selectedTranscript?.get('id'),
78
- geneName:
79
- selectedTranscript?.get('gene_name') ??
80
- selectedTranscript?.get('parentId'),
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
- return {
84
- ...result,
85
- treeMetadata: treeMetadataJson,
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
- if (taxInfo?.sciname) {
97
- metadata['Scientific name'] = taxInfo.sciname
98
- }
99
- if (taxInfo?.commonName) {
100
- metadata['Common name'] = taxInfo.commonName
101
- }
102
- if (desc.accession) {
103
- metadata.Accession = desc.accession
104
- }
105
- if (desc.id) {
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 (!connectedView?.initialized || !hasHoverPosition(hovered)) {
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
- export interface BlastParams {
48
- blastDatabase: BlastDatabase
49
- msaAlgorithm: MsaAlgorithm
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 = {