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
@@ -11,11 +11,6 @@ import {
11
11
  import { observer } from 'mobx-react'
12
12
  import { makeStyles } from 'tss-react/mui'
13
13
 
14
- import CachedBlastResults from './CachedBlastResults'
15
- import MsaAlgorithmSelect from './MsaAlgorithmSelect'
16
- import { blastLaunchView } from './blastLaunchView'
17
- import { blastDatabaseOptions, defaultBlastDatabase } from './consts'
18
- import { useCachedBlastResults } from './useCachedBlastResults'
19
14
  import TextField2 from '../../../components/TextField2'
20
15
  import {
21
16
  getBlastViewTitle,
@@ -26,14 +21,30 @@ import LaunchPanelContent from '../LaunchPanelContent'
26
21
  import SubmitCancelActions from '../SubmitCancelActions'
27
22
  import TranscriptSelector from '../TranscriptSelector'
28
23
  import { useTranscriptSelection } from '../useTranscriptSelection'
24
+ import CachedBlastResults from './CachedBlastResults'
25
+ import MsaAlgorithmSelect from './MsaAlgorithmSelect'
26
+ import { blastLaunchView } from './blastLaunchView'
27
+ import {
28
+ databaseOptionsFor,
29
+ defaultBlastDatabase,
30
+ defaultSearchFor,
31
+ searchPrograms,
32
+ } from './consts'
33
+ import { useCachedBlastResults } from './useCachedBlastResults'
29
34
 
30
- import type { BlastDatabase, MsaAlgorithm } from './consts'
35
+ import type { MsaAlgorithm, SearchChoice, SearchProgram } from './consts'
31
36
  import type { AbstractTrackModel, Feature } from '@jbrowse/core/util'
32
37
 
33
38
  const useStyles = makeStyles()({
34
39
  selectField: {
35
40
  width: 150,
36
41
  },
42
+ // wider than the rest because the values are what the user came to read, and
43
+ // `uniprotkb_swissprot` is 19 characters — at 150 the field showed
44
+ // `uniprotkb_swis…`, which does not distinguish it from `uniprotkb_trembl`
45
+ databaseField: {
46
+ width: 230,
47
+ },
37
48
  cachedResultsAccordion: {
38
49
  marginTop: 20,
39
50
  },
@@ -56,10 +67,16 @@ const BlastAutomaticPanel = observer(function ({
56
67
  const { classes } = useStyles()
57
68
  const view = getLinearGenomeView(model)
58
69
  const [launchViewError, setLaunchViewError] = useState<unknown>()
59
- const [selectedBlastDatabase, setSelectedBlastDatabase] =
60
- useState<BlastDatabase>(defaultBlastDatabase)
70
+ // one piece of state, not two: a program and a database that program does not
71
+ // have is a 400 from EBI minutes after Submit, and holding them apart is what
72
+ // would let them drift into that
73
+ const [search, setSearch] = useState<SearchChoice>({
74
+ program: 'blastp',
75
+ database: defaultBlastDatabase,
76
+ })
61
77
  const [selectedMsaAlgorithm, setSelectedMsaAlgorithm] =
62
78
  useState<MsaAlgorithm>('clustalo')
79
+ const isPhmmer = search.program === 'phmmer'
63
80
 
64
81
  const geneIds = useMemo(() => getGeneIdentifiers(feature), [feature])
65
82
  const { results: cachedResults, error: cachedResultsError } =
@@ -74,37 +91,69 @@ const BlastAutomaticPanel = observer(function ({
74
91
  {children}
75
92
  <TextField2
76
93
  variant="outlined"
77
- label="BLAST database"
94
+ label="Search program"
78
95
  className={classes.selectField}
79
96
  select
80
- value={selectedBlastDatabase}
97
+ value={search.program}
81
98
  onChange={event => {
82
- setSelectedBlastDatabase(event.target.value as BlastDatabase)
99
+ // the two services name their databases differently, so switching
100
+ // program replaces the database rather than keeping a name the new
101
+ // one has never heard of
102
+ setSearch(defaultSearchFor(event.target.value as SearchProgram))
83
103
  }}
84
104
  >
85
- {blastDatabaseOptions.map(val => (
105
+ {searchPrograms.map(val => (
86
106
  <MenuItem value={val} key={val}>
87
107
  {val}
88
108
  </MenuItem>
89
109
  ))}
90
110
  </TextField2>
91
111
 
92
- <MsaAlgorithmSelect
93
- className={classes.selectField}
94
- value={selectedMsaAlgorithm}
95
- onChange={setSelectedMsaAlgorithm}
96
- />
112
+ <TextField2
113
+ variant="outlined"
114
+ label="Database"
115
+ className={classes.databaseField}
116
+ select
117
+ value={search.database}
118
+ onChange={event => {
119
+ setSearch({
120
+ program: search.program,
121
+ database: event.target.value,
122
+ } as SearchChoice)
123
+ }}
124
+ >
125
+ {databaseOptionsFor(search.program).map(val => (
126
+ <MenuItem value={val} key={val}>
127
+ {val}
128
+ </MenuItem>
129
+ ))}
130
+ </TextField2>
131
+
132
+ {isPhmmer ? null : (
133
+ <MsaAlgorithmSelect
134
+ className={classes.selectField}
135
+ value={selectedMsaAlgorithm}
136
+ onChange={setSelectedMsaAlgorithm}
137
+ />
138
+ )}
97
139
 
98
140
  <TranscriptSelector feature={feature} {...transcriptSelection} />
99
141
 
100
142
  <Typography className={classes.infoText}>
101
- This panel will automatically submit a blastp query to EBI, which
102
- searches UniProtKB. Searches usually finish in under a minute, and
103
- swissprot returns curated sequences that align more cleanly than the
104
- many near-identical entries a TrEMBL search brings back. After
105
- completion, all the hits will be run through a multiple sequence
106
- alignment. Searching NCBI's nr needs the manual approach: NCBI no
107
- longer lets a browser read responses from Blast.cgi.
143
+ {isPhmmer
144
+ ? `phmmer searches UniProtKB with a profile HMM built from the query,
145
+ so it aligns the hits as it finds them and that alignment is used
146
+ directly — nothing is realigned afterwards. The tree is then built
147
+ from it by neighbour-joining. A hit matching the query in more
148
+ than one place appears once per matched region.`
149
+ : `This panel will automatically submit a blastp query to EBI, which
150
+ searches UniProtKB. Searches usually finish in under a minute, and
151
+ swissprot returns curated sequences that align more cleanly than
152
+ the many near-identical entries a TrEMBL search brings back. After
153
+ completion, all the hits will be run through a multiple sequence
154
+ alignment.`}{' '}
155
+ Searching NCBI's nr needs the manual approach: NCBI no longer lets a
156
+ browser read responses from Blast.cgi.
108
157
  </Typography>
109
158
 
110
159
  {cachedResults.length > 0 ? (
@@ -133,12 +182,21 @@ const BlastAutomaticPanel = observer(function ({
133
182
  feature: selectedTranscript,
134
183
  view,
135
184
  newViewTitle: getBlastViewTitle(feature, selectedTranscript),
136
- blastParams: {
137
- blastDatabase: selectedBlastDatabase,
138
- msaAlgorithm: selectedMsaAlgorithm,
139
- selectedTranscript,
140
- proteinSequence,
141
- },
185
+ blastParams:
186
+ search.program === 'phmmer'
187
+ ? {
188
+ searchProgram: 'phmmer',
189
+ blastDatabase: search.database,
190
+ selectedTranscript,
191
+ proteinSequence,
192
+ }
193
+ : {
194
+ searchProgram: 'blastp',
195
+ blastDatabase: search.database,
196
+ msaAlgorithm: selectedMsaAlgorithm,
197
+ selectedTranscript,
198
+ proteinSequence,
199
+ },
142
200
  })
143
201
  handleClose()
144
202
  }
@@ -5,7 +5,6 @@ import { Alert, Typography } from '@mui/material'
5
5
  import { observer } from 'mobx-react'
6
6
  import { makeStyles } from 'tss-react/mui'
7
7
 
8
- import { BASE_BLAST_URL } from './consts'
9
8
  import ExternalLink from '../../../components/ExternalLink'
10
9
  import TextField2 from '../../../components/TextField2'
11
10
  import { useQueryRowName } from '../../useQueryRowName'
@@ -20,6 +19,7 @@ import QueryRowSelector from '../QueryRowSelector'
20
19
  import SubmitCancelActions from '../SubmitCancelActions'
21
20
  import TranscriptSelector from '../TranscriptSelector'
22
21
  import { useTranscriptSelection } from '../useTranscriptSelection'
22
+ import { BASE_BLAST_URL } from './consts'
23
23
 
24
24
  import type { AbstractTrackModel, Feature } from '@jbrowse/core/util'
25
25
 
@@ -4,15 +4,15 @@ import SettingsIcon from '@mui/icons-material/Settings'
4
4
  import { IconButton } from '@mui/material'
5
5
  import { makeStyles } from 'tss-react/mui'
6
6
 
7
- import BlastAutomaticPanel from './BlastAutomaticPanel'
8
- import BlastManualPanel from './BlastManualPanel'
9
- import BlastMethodSelector from './BlastMethodSelector'
10
- import BlastSettingsDialog from './BlastSettingsDialog'
11
7
  import {
12
8
  DEFAULT_EBI_EMAIL,
13
9
  EBI_EMAIL_STORAGE_KEY,
14
10
  } from '../../../utils/ebiJobDispatcher'
15
11
  import { useLocalStorage } from '../../../utils/useLocalStorage'
12
+ import BlastAutomaticPanel from './BlastAutomaticPanel'
13
+ import BlastManualPanel from './BlastManualPanel'
14
+ import BlastMethodSelector from './BlastMethodSelector'
15
+ import BlastSettingsDialog from './BlastSettingsDialog'
16
16
 
17
17
  import type { AbstractTrackModel, Feature } from '@jbrowse/core/util'
18
18
 
@@ -0,0 +1,50 @@
1
+ import { expect, test } from 'vitest'
2
+
3
+ import { describeSearch } from './CachedBlastResults'
4
+
5
+ import type { CachedBlastResult } from '../../../utils/blastCache'
6
+
7
+ const base: CachedBlastResult = {
8
+ id: 'k',
9
+ proteinSequence: 'MKV',
10
+ blastDatabase: 'uniprotkb_swissprot',
11
+ msa: '',
12
+ tree: '',
13
+ treeMetadata: '{}',
14
+ rid: 'r',
15
+ timestamp: 0,
16
+ }
17
+
18
+ function row(fields: Partial<CachedBlastResult>): CachedBlastResult {
19
+ return { ...base, ...fields }
20
+ }
21
+
22
+ test('a blastp row names its database, program and aligner', () => {
23
+ expect(
24
+ describeSearch(row({ searchProgram: 'blastp', msaAlgorithm: 'muscle' })),
25
+ ).toBe('uniprotkb_swissprot / blastp / muscle')
26
+ })
27
+
28
+ // phmmer aligns as it searches, so its rows carry no msaAlgorithm at all --
29
+ // which read as "(undefined)" while this assumed one
30
+ test('a phmmer row names no aligner, because none ran', () => {
31
+ expect(
32
+ describeSearch(
33
+ row({ blastDatabase: 'swissprot', searchProgram: 'phmmer' }),
34
+ ),
35
+ ).toBe('swissprot / phmmer')
36
+ })
37
+
38
+ test('a row cached before searchProgram existed reads as blastp', () => {
39
+ expect(describeSearch(row({ msaAlgorithm: 'clustalo' }))).toBe(
40
+ 'uniprotkb_swissprot / blastp / clustalo',
41
+ )
42
+ })
43
+
44
+ test('a row from the NCBI era keeps the program it recorded', () => {
45
+ expect(
46
+ describeSearch(
47
+ row({ blastProgram: 'quick-blastp', msaAlgorithm: 'clustalo' }),
48
+ ),
49
+ ).toBe('uniprotkb_swissprot / quick-blastp / clustalo')
50
+ })
@@ -14,14 +14,14 @@ import {
14
14
  import { observer } from 'mobx-react'
15
15
  import { makeStyles } from 'tss-react/mui'
16
16
 
17
- import { blastLaunchViewFromCache } from './blastLaunchView'
18
- import { useCachedBlastResults } from './useCachedBlastResults'
19
17
  import {
20
18
  featureMatchesId,
21
19
  getGeneIdentifiers,
22
20
  getLinearGenomeView,
23
21
  getSortedTranscriptFeatures,
24
22
  } from '../../util'
23
+ import { blastLaunchViewFromCache } from './blastLaunchView'
24
+ import { useCachedBlastResults } from './useCachedBlastResults'
25
25
 
26
26
  import type { CachedBlastResult } from '../../../utils/blastCache'
27
27
  import type { AbstractTrackModel, Feature } from '@jbrowse/core/util'
@@ -51,6 +51,26 @@ function getResultDisplayName(result: CachedBlastResult): string {
51
51
  : (result.geneId ?? result.transcriptId ?? 'Unknown')
52
52
  }
53
53
 
54
+ /**
55
+ * How the row was produced: `uniprotkb_swissprot / blastp / clustalo`, or
56
+ * `swissprot / phmmer` for a row phmmer aligned as it searched and that
57
+ * therefore ran no aligner. Each part is dropped when absent rather than
58
+ * printed empty — `msaAlgorithm` became optional when phmmer arrived, and a
59
+ * phmmer row read `(undefined)` until this stopped assuming one.
60
+ *
61
+ * `blastProgram` is the older field, written only while the plugin still
62
+ * queried NCBI directly and blastp/quick-blastp was a real choice.
63
+ */
64
+ export function describeSearch(result: CachedBlastResult) {
65
+ return [
66
+ result.blastDatabase,
67
+ result.searchProgram ?? result.blastProgram ?? 'blastp',
68
+ result.msaAlgorithm,
69
+ ]
70
+ .filter(Boolean)
71
+ .join(' / ')
72
+ }
73
+
54
74
  const CachedBlastResults = observer(function ({
55
75
  model,
56
76
  handleClose,
@@ -149,7 +169,7 @@ const CachedBlastResults = observer(function ({
149
169
  }}
150
170
  >
151
171
  <ListItemText
152
- primary={`${getResultDisplayName(result)} - ${result.blastDatabase}${result.blastProgram ? `/${result.blastProgram}` : ''} (${result.msaAlgorithm})`}
172
+ primary={`${getResultDisplayName(result)} - ${describeSearch(result)}`}
153
173
  secondary={`${new Date(result.timestamp).toLocaleString()} - Seq: ${result.proteinSequence.slice(0, 30)}...`}
154
174
  />
155
175
  </ListItemButton>
@@ -2,8 +2,8 @@ import React from 'react'
2
2
 
3
3
  import { MenuItem } from '@mui/material'
4
4
 
5
- import { msaAlgorithms } from './consts'
6
5
  import TextField2 from '../../../components/TextField2'
6
+ import { msaAlgorithms } from './consts'
7
7
 
8
8
  import type { MsaAlgorithm } from './consts'
9
9
 
@@ -27,3 +27,43 @@ export type BlastDatabase = (typeof blastDatabaseOptions)[number]
27
27
  // curated, so it returns roughly one good sequence per species rather than the
28
28
  // many near-identical TrEMBL entries an alignment reads poorly
29
29
  export const defaultBlastDatabase: BlastDatabase = 'uniprotkb_swissprot'
30
+
31
+ export const searchPrograms = ['blastp', 'phmmer'] as const
32
+ export type SearchProgram = (typeof searchPrograms)[number]
33
+
34
+ /**
35
+ * phmmer offers PDB, AlphaFold, Ensembl Genomes, MEROPS and ChEMBL too, but
36
+ * targets outside UniProt carry no OS=/OX= in their description, so those rows
37
+ * would lose their species and common name. Only the databases that label their
38
+ * hits are offered.
39
+ */
40
+ export const phmmerDatabaseOptions = [
41
+ 'swissprot',
42
+ 'uniprotkb',
43
+ 'uniprotrefprot',
44
+ ] as const
45
+ export type PhmmerDatabase = (typeof phmmerDatabaseOptions)[number]
46
+
47
+ export const defaultPhmmerDatabase: PhmmerDatabase = 'swissprot'
48
+
49
+ /**
50
+ * A program together with a database that program actually has.
51
+ *
52
+ * The pair travels as one value because neither service knows the other's
53
+ * database names — `swissprot` is a phmmer database and `uniprotkb_swissprot` a
54
+ * blastp one — so a program held apart from its database can drift into a
55
+ * combination EBI answers with a 400, minutes after the user pressed Submit.
56
+ */
57
+ export type SearchChoice =
58
+ | { program: 'blastp'; database: BlastDatabase }
59
+ | { program: 'phmmer'; database: PhmmerDatabase }
60
+
61
+ export function defaultSearchFor(program: SearchProgram): SearchChoice {
62
+ return program === 'phmmer'
63
+ ? { program, database: defaultPhmmerDatabase }
64
+ : { program, database: defaultBlastDatabase }
65
+ }
66
+
67
+ export function databaseOptionsFor(program: SearchProgram) {
68
+ return program === 'phmmer' ? phmmerDatabaseOptions : blastDatabaseOptions
69
+ }
@@ -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,11 +4,6 @@ import { Typography } from '@mui/material'
4
4
  import { observer } from 'mobx-react'
5
5
  import { makeStyles } from 'tss-react/mui'
6
6
 
7
- import OrthologSourceSelect, {
8
- ORTHOLOG_SOURCE_STORAGE_KEY,
9
- } from './OrthologSourceSelect'
10
- import QuerySpeciesSelect from './QuerySpeciesSelect'
11
- import { orthologLaunchView } from './orthologLaunchView'
12
7
  import TextField2 from '../../../components/TextField2'
13
8
  import { defaultMaxSpecies } from '../../../utils/ncbiOrthologs'
14
9
  import { useLocalStorage } from '../../../utils/useLocalStorage'
@@ -23,6 +18,11 @@ import LaunchPanelContent from '../LaunchPanelContent'
23
18
  import SubmitCancelActions from '../SubmitCancelActions'
24
19
  import TranscriptSelector from '../TranscriptSelector'
25
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'
26
26
 
27
27
  import type { OrthologSource } from '../../../MsaViewPanel/model'
28
28
  import type { MsaAlgorithm } from '../BlastQuery/consts'
@@ -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 { detectQueryRow, getMsaRowNames } from './detectQueryRow'
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('detectQueryRow', () => {
21
+ describe('findQueryRow', () => {
22
22
  test('finds the query by sequence when the aligner renamed it', () => {
23
- expect(detectQueryRow(clustal, protein)).toMatchObject({
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(detectQueryRow(fasta, protein)?.name).toBe('Query_1')
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(detectQueryRow(clustal, `${protein}*`)?.name).toBe('Query_1')
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(detectQueryRow(trimmed, protein)).toMatchObject({
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
- detectQueryRow(homologsOnly, 'WWWWWWWWWWWWWWWWWWWWWWWWWWWWWW'),
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(detectQueryRow('>partial\nMKWV', protein)).toBeUndefined()
60
- expect(detectQueryRow('not an alignment at all', protein)).toBeUndefined()
61
- expect(detectQueryRow('', protein)).toBeUndefined()
62
- expect(detectQueryRow(clustal, '')).toBeUndefined()
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('getMsaRowNames', () => {
68
+ describe('findQueryRow row names', () => {
67
69
  test('lists the rows for the override dropdown', () => {
68
- expect(getMsaRowNames(clustal)).toEqual([
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(getMsaRowNames('CLUSTAL W')).toEqual([])
77
- expect(getMsaRowNames('')).toEqual([])
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 function detectQueryRow(
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
- ): QueryRowMatch | undefined {
72
- const query = normalize(proteinSequence)
73
- if (!query || !msaText.trim()) {
74
- return undefined
86
+ ): MsaQueryRow {
87
+ if (!msaText.trim()) {
88
+ return { names: [] }
75
89
  }
76
90
 
77
- let names: string[]
78
- let parsed: { getRow: (name: string) => string }
91
+ let parsed
79
92
  try {
80
- const msa = parseMSA(msaText)
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 undefined
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
- const order: MatchQuality[] = ['exact', 'partial', 'similar']
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 { detectQueryRow, getMsaRowNames } from './detectQueryRow'
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 { detected, names } = useMemo(
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 ?? detected?.name ?? '',
26
+ querySeqName: override ?? match?.name ?? '',
30
27
  setQuerySeqName: setOverride,
31
- isAutoDetected: override === undefined && !!detected,
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
@@ -1,8 +1,9 @@
1
- import type { ReactNode } from 'react'
2
1
  import React, { Component } from 'react'
3
2
 
4
3
  import { ErrorMessage } from '@jbrowse/core/ui'
5
4
 
5
+ import type { ReactNode } from 'react'
6
+
6
7
  interface Props {
7
8
  children: ReactNode
8
9
  }