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
@@ -28,6 +28,7 @@ function makeModel() {
28
28
  const calls: (number | undefined)[] = []
29
29
  const model = {
30
30
  querySeqName: 'hg38.chr1',
31
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
31
32
  transcriptToMsaMap: undefined,
32
33
  mafRegion,
33
34
  connectedView: { initialized: true, assemblyNames: ['hg38'] },
@@ -1,3 +1,21 @@
1
+ /**
2
+ * Whether `querySeqName` names a row this alignment actually has.
3
+ *
4
+ * react-msaview's `seqPosToGlobalCol` answers 0 for a name it does not know, so
5
+ * without this every genome position maps to the first column and hovering the
6
+ * genome — or a connected structure — lights column 0 of an unrelated row. The
7
+ * name is wrong more often than it looks: it defaults to `QUERY`, which an
8
+ * uploaded alignment has no reason to carry, and the manual panel leaves it
9
+ * empty when it cannot match the protein to a row.
10
+ *
11
+ * The other direction has no such hole: msaCoordToGenomeRegions needs the query
12
+ * row's sequence to map a column at all, so a missing row is already nothing
13
+ * there.
14
+ */
15
+ export function hasQueryRow(model: { rows: string[][]; querySeqName: string }) {
16
+ return model.rows.some(r => r[0] === model.querySeqName)
17
+ }
18
+
1
19
  export function hasHoverPosition(
2
20
  hovered: unknown,
3
21
  ): hovered is { hoverPosition: { coord: number; refName: string } } {
@@ -3,6 +3,8 @@ import { createDbOpener } from './idb'
3
3
  import type {
4
4
  BlastDatabase,
5
5
  MsaAlgorithm,
6
+ PhmmerDatabase,
7
+ SearchProgram,
6
8
  } from '../LaunchMsaView/components/BlastQuery/consts'
7
9
  import type { DBSchema } from 'idb'
8
10
 
@@ -13,14 +15,17 @@ const DB_VERSION = 2
13
15
  export interface CachedBlastResult {
14
16
  id: string
15
17
  proteinSequence: string
16
- blastDatabase: BlastDatabase
18
+ blastDatabase: BlastDatabase | PhmmerDatabase
17
19
  /**
18
20
  * Only ever set on rows cached by a version that still queried NCBI, where
19
21
  * the choice between blastp and quick-blastp was real. Kept so those rows
20
22
  * still display; never written now.
21
23
  */
22
24
  blastProgram?: string
23
- msaAlgorithm: MsaAlgorithm
25
+ /** absent on rows cached before phmmer existed, which were all blastp */
26
+ searchProgram?: SearchProgram
27
+ /** absent on phmmer rows, which are aligned by the search itself */
28
+ msaAlgorithm?: MsaAlgorithm
24
29
  msa: string
25
30
  tree: string
26
31
  treeMetadata: string
@@ -52,13 +57,25 @@ const getDB = createDbOpener<BlastCacheDB>(
52
57
  },
53
58
  )
54
59
 
55
- function createCacheKey(
56
- proteinSequence: string,
57
- blastDatabase: BlastDatabase,
58
- msaAlgorithm: MsaAlgorithm,
59
- transcriptId?: string,
60
- ) {
60
+ function createCacheKey({
61
+ proteinSequence,
62
+ blastDatabase,
63
+ msaAlgorithm,
64
+ searchProgram,
65
+ transcriptId,
66
+ }: {
67
+ proteinSequence: string
68
+ blastDatabase: BlastDatabase | PhmmerDatabase
69
+ msaAlgorithm?: MsaAlgorithm
70
+ searchProgram?: SearchProgram
71
+ transcriptId?: string
72
+ }) {
61
73
  const idPart = transcriptId ? `:${transcriptId}` : ''
74
+ // phmmer keys are prefixed and blastp keys are left exactly as they were, so
75
+ // results cached before phmmer existed still resolve
76
+ if (searchProgram === 'phmmer') {
77
+ return `phmmer:${blastDatabase}${idPart}:${proteinSequence}`
78
+ }
62
79
  // msaAlgorithm is part of the key because the stored msa/tree are produced by
63
80
  // it — without it, re-running the same query under a different algorithm
64
81
  // overwrites the earlier result and drops it from the history list
@@ -69,6 +86,7 @@ export async function saveBlastResult({
69
86
  proteinSequence,
70
87
  blastDatabase,
71
88
  msaAlgorithm,
89
+ searchProgram,
72
90
  msa,
73
91
  tree,
74
92
  treeMetadata,
@@ -79,8 +97,9 @@ export async function saveBlastResult({
79
97
  geneName,
80
98
  }: {
81
99
  proteinSequence: string
82
- blastDatabase: BlastDatabase
83
- msaAlgorithm: MsaAlgorithm
100
+ blastDatabase: BlastDatabase | PhmmerDatabase
101
+ msaAlgorithm?: MsaAlgorithm
102
+ searchProgram?: SearchProgram
84
103
  msa: string
85
104
  tree: string
86
105
  treeMetadata: string
@@ -91,17 +110,19 @@ export async function saveBlastResult({
91
110
  geneName?: string
92
111
  }) {
93
112
  const db = await getDB()
94
- const id = createCacheKey(
113
+ const id = createCacheKey({
95
114
  proteinSequence,
96
115
  blastDatabase,
97
116
  msaAlgorithm,
117
+ searchProgram,
98
118
  transcriptId,
99
- )
119
+ })
100
120
  const entry: CachedBlastResult = {
101
121
  id,
102
122
  proteinSequence,
103
123
  blastDatabase,
104
124
  msaAlgorithm,
125
+ searchProgram,
105
126
  msa,
106
127
  tree,
107
128
  treeMetadata,
@@ -1,7 +1,7 @@
1
1
  import { fetchEbiResult, submitEbiJob, waitForEbiJob } from './ebiJobDispatcher'
2
2
 
3
- import type { BlastHit } from './types'
4
3
  import type { BlastDatabase } from '../LaunchMsaView/components/BlastQuery/consts'
4
+ import type { BlastHit } from './types'
5
5
 
6
6
  const TOOL = 'ncbiblast'
7
7
 
package/src/utils/msa.ts CHANGED
@@ -32,6 +32,43 @@ const algorithms: Record<
32
32
  },
33
33
  }
34
34
 
35
+ /**
36
+ * Build a tree from an alignment that already exists, which is what the phmmer
37
+ * path needs: phmmer produces the alignment itself, so there is no aligner run
38
+ * to take a guide tree from — and a guide tree is a byproduct of deciding
39
+ * progressive alignment order, not a phylogeny, so it is not what we would want
40
+ * even if there were one. simple_phylogeny is clustalw2's neighbour-joining on
41
+ * a real distance matrix, Kimura-corrected for protein distances.
42
+ */
43
+ export async function launchTree({
44
+ alignment,
45
+ onProgress,
46
+ }: {
47
+ alignment: string
48
+ onProgress: (arg: string) => void
49
+ }) {
50
+ const tool = 'simple_phylogeny'
51
+ onProgress('Building tree...')
52
+
53
+ const jobId = await submitEbiJob({
54
+ tool,
55
+ params: {
56
+ sequence: alignment,
57
+ tree: 'phylip',
58
+ clustering: 'Neighbour-joining',
59
+ kimura: 'true',
60
+ },
61
+ })
62
+ await waitForEbiJob({
63
+ tool,
64
+ jobId,
65
+ onCountdown: s => {
66
+ onProgress(`Re-checking tree status in... ${s}`)
67
+ },
68
+ })
69
+ return fetchEbiResult({ tool, jobId, type: 'tree' })
70
+ }
71
+
35
72
  export async function launchMSA({
36
73
  algorithm,
37
74
  sequence,
@@ -56,16 +93,10 @@ export async function launchMSA({
56
93
  onProgress(`Re-checking MSA status in... ${s}`)
57
94
  },
58
95
  })
59
- return {
60
- msa: await fetchEbiResult({
61
- tool: algorithm,
62
- jobId,
63
- type: config.msaResult,
64
- }),
65
- tree: await fetchEbiResult({
66
- tool: algorithm,
67
- jobId,
68
- type: config.treeResult,
69
- }),
70
- }
96
+ // one finished job, two result files, neither derived from the other
97
+ const [msa, tree] = await Promise.all([
98
+ fetchEbiResult({ tool: algorithm, jobId, type: config.msaResult }),
99
+ fetchEbiResult({ tool: algorithm, jobId, type: config.treeResult }),
100
+ ])
101
+ return { msa, tree }
71
102
  }
@@ -0,0 +1,95 @@
1
+ import { makeId } from '../LaunchMsaView/components/util'
2
+
3
+ import type { PhmmerRow } from './phmmer'
4
+ import type { TaxonomyInfo } from './taxonomyNames'
5
+ import type { BlastHitDescription } from './types'
6
+
7
+ /**
8
+ * Turning search results into the rows the view is given, kept free of any
9
+ * jbrowse or network import so the whole assembly can be run and checked
10
+ * outside a browser — see test/phmmerLive.test.ts.
11
+ */
12
+ export function buildRowMetadata(
13
+ desc: BlastHitDescription,
14
+ taxonomyInfo: Map<number, TaxonomyInfo>,
15
+ ) {
16
+ const metadata: Record<string, string> = {}
17
+ const taxInfo = desc.taxid ? taxonomyInfo.get(desc.taxid) : undefined
18
+
19
+ if (taxInfo?.sciname) {
20
+ metadata['Scientific name'] = taxInfo.sciname
21
+ }
22
+ if (taxInfo?.commonName) {
23
+ metadata['Common name'] = taxInfo.commonName
24
+ }
25
+ if (desc.accession) {
26
+ metadata.Accession = desc.accession
27
+ }
28
+ if (desc.id) {
29
+ metadata.ID = desc.id
30
+ }
31
+ if (desc.title) {
32
+ metadata.Description = desc.title
33
+ }
34
+
35
+ return metadata
36
+ }
37
+
38
+ /**
39
+ * One target can match the query in several places and phmmer emits a row per
40
+ * matched envelope — four for lamprey albumin against human albumin, which has
41
+ * three domains. Those rows share an accession and so would share a name, and
42
+ * duplicate names silently collapse rows in both the MSA and the tree, so the
43
+ * envelope disambiguates them.
44
+ */
45
+ export function makeRowNames(
46
+ rows: PhmmerRow[],
47
+ taxonomyInfo: Map<number, TaxonomyInfo>,
48
+ ) {
49
+ const baseNames = rows.map(row => makeId(row, taxonomyInfo))
50
+ const counts = new Map<string, number>()
51
+ for (const name of baseNames) {
52
+ counts.set(name, (counts.get(name) ?? 0) + 1)
53
+ }
54
+
55
+ const used = new Set<string>()
56
+ return baseNames.map((base, i) => {
57
+ let name =
58
+ counts.get(base)! > 1 ? `${base}_${rows[i]!.range ?? i + 1}` : base
59
+ while (used.has(name)) {
60
+ name = `${name}_${i + 1}`
61
+ }
62
+ used.add(name)
63
+ return name
64
+ })
65
+ }
66
+
67
+ /**
68
+ * The phmmer alignment as the view receives it: aligned FASTA whose first row
69
+ * is the query, plus the per-row metadata keyed by the same names, which are
70
+ * also what the tree's leaves are labelled with.
71
+ */
72
+ export function buildPhmmerMsa({
73
+ rows,
74
+ queryRow,
75
+ taxonomyInfo,
76
+ querySeqName = 'QUERY',
77
+ }: {
78
+ rows: PhmmerRow[]
79
+ queryRow: string
80
+ taxonomyInfo: Map<number, TaxonomyInfo>
81
+ querySeqName?: string
82
+ }) {
83
+ const treeMetadata: Record<string, Record<string, string>> = {}
84
+ const rowNames = makeRowNames(rows, taxonomyInfo)
85
+ const sequences = rows.map((row, i) => {
86
+ const rowName = rowNames[i]!
87
+ treeMetadata[rowName] = buildRowMetadata(row, taxonomyInfo)
88
+ return `>${rowName}\n${row.aligned}`
89
+ })
90
+
91
+ return {
92
+ msa: [`>${querySeqName}\n${queryRow}`, ...sequences].join('\n'),
93
+ treeMetadata,
94
+ }
95
+ }
@@ -0,0 +1,174 @@
1
+ import { StockholmMSA } from 'msa-parsers'
2
+
3
+ import { fetchEbiResult, submitEbiJob, waitForEbiJob } from './ebiJobDispatcher'
4
+
5
+ import type { PhmmerDatabase } from '../LaunchMsaView/components/BlastQuery/consts'
6
+
7
+ const TOOL = 'hmmer3_phmmer'
8
+
9
+ export interface PhmmerRow {
10
+ accession: string
11
+ id: string
12
+ sciname: string
13
+ taxid?: number
14
+ title?: string
15
+ /** the matched envelope on the target, e.g. '503-912', absent if unparseable */
16
+ range?: string
17
+ /** the row as phmmer aligned it, uppercased with '.' inserts turned into '-' */
18
+ aligned: string
19
+ }
20
+
21
+ export interface PhmmerAlignment {
22
+ rows: PhmmerRow[]
23
+ /**
24
+ * the query, placed into the same columns. phmmer does not put the query in
25
+ * its own output, so this is derived — see buildQueryRow.
26
+ */
27
+ queryRow: string
28
+ }
29
+
30
+ /**
31
+ * Human-facing link to a job, shown while it runs and on error.
32
+ *
33
+ * The category has to be sss: jdispatcher serves its shell with a 200 for any
34
+ * category, so /pfa/ and /psa/ look fine to a fetch and render "Page Not Found"
35
+ * in a browser.
36
+ */
37
+ export function phmmerResultUrl(jobId: string) {
38
+ return `https://www.ebi.ac.uk/jdispatcher/sss/${TOOL}/summary?jobId=${jobId}`
39
+ }
40
+
41
+ /** EBI job ids are prefixed with the tool that made them */
42
+ export function isPhmmerJobId(jobId: string) {
43
+ return jobId.startsWith(`${TOOL}-`)
44
+ }
45
+
46
+ /**
47
+ * phmmer aligns every hit to a profile built from the query, one match state
48
+ * per query residue, and marks those columns 'x' in #=GC RF. So the query's own
49
+ * row is exactly recoverable: walk RF, consume a query residue at each match
50
+ * column, gap everywhere else.
51
+ *
52
+ * This is the one piece of real logic here rather than a library call, and it
53
+ * is checked hard: if the match columns do not account for the query exactly,
54
+ * the columns and the query have drifted apart, and a query row that is off by
55
+ * even one residue would silently mis-map every column to the genome. Throwing
56
+ * is much better than drawing that.
57
+ */
58
+ function buildQueryRow({ rf, query }: { rf: string; query: string }) {
59
+ let consumed = 0
60
+ const row = Array.from(rf, c =>
61
+ c === 'x' ? (query[consumed++] ?? '-') : '-',
62
+ ).join('')
63
+ if (consumed !== query.length) {
64
+ throw new Error(
65
+ `phmmer alignment has ${consumed} match columns for a query of ${query.length} residues, so the query row cannot be placed`,
66
+ )
67
+ }
68
+ return row
69
+ }
70
+
71
+ /**
72
+ * '[subseq from] Albumin OS=Homo sapiens OX=9606 GN=ALB PE=1 SV=2' is what a
73
+ * UniProt target's #=GS DE looks like. Hits from the non-UniProt databases
74
+ * phmmer also offers (PDB, AlphaFold, MEROPS...) carry no OS=/OX= at all, so
75
+ * every field here is optional.
76
+ */
77
+ function parseDescription(de: string | undefined) {
78
+ const text = (de ?? '').replace('[subseq from] ', '')
79
+ const sciname = /OS=(.*?)\s+(?:OX|GN|PE|SV)=/.exec(text)?.[1]
80
+ const ox = /OX=(\d+)/.exec(text)?.[1]
81
+ return {
82
+ sciname: sciname ?? 'unknown',
83
+ taxid: ox ? Number.parseInt(ox, 10) : undefined,
84
+ title: text.split(' OS=')[0] || undefined,
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Target names look like 'sp|P02768|ALBU_HUMAN/1-609' for UniProt databases and
90
+ * like anything at all for the others, so an unrecognized name becomes its own
91
+ * accession rather than being dropped.
92
+ */
93
+ function parseName(name: string) {
94
+ const slash = name.lastIndexOf('/')
95
+ const range =
96
+ slash === -1 ? undefined : /^\d+-\d+$/.exec(name.slice(slash + 1))?.[0]
97
+ const bare = range === undefined ? name : name.slice(0, slash)
98
+ const parts = bare.split('|')
99
+ return parts.length === 3
100
+ ? { accession: parts[1]!, id: parts[2]!, range }
101
+ : { accession: bare, id: bare, range }
102
+ }
103
+
104
+ /**
105
+ * Exported for testing against a captured .sto — the annotation names (RF, the
106
+ * DE line's OS=/OX=) are the whole risk in this mapping, and nothing else in CI
107
+ * would notice if HMMER or EBI changed one.
108
+ */
109
+ export function parsePhmmerAlignment({
110
+ stockholm,
111
+ query,
112
+ }: {
113
+ stockholm: string
114
+ query: string
115
+ }): PhmmerAlignment {
116
+ const { gc, gs, seqdata, seqname } = new StockholmMSA(stockholm, 0).getMSA()
117
+ const rf = gc.RF
118
+ if (!rf) {
119
+ throw new Error('phmmer alignment has no #=GC RF line')
120
+ }
121
+
122
+ return {
123
+ queryRow: buildQueryRow({ rf, query }),
124
+ rows: seqname.map(name => ({
125
+ ...parseName(name),
126
+ ...parseDescription(gs.DE?.[name]?.[0]),
127
+ // insert columns come back lowercase with '.' for gaps; the MSA renderer
128
+ // looks colors up by the literal letter, so lowercase would draw
129
+ // uncolored. The insert columns stay visible as gaps in the query row.
130
+ aligned: (seqdata[name] ?? '').replaceAll('.', '-').toUpperCase(),
131
+ })),
132
+ }
133
+ }
134
+
135
+ export async function queryPhmmer({
136
+ query,
137
+ database,
138
+ onProgress,
139
+ onRid,
140
+ }: {
141
+ query: string
142
+ database: PhmmerDatabase
143
+ onProgress: (arg: string) => void
144
+ onRid: (arg: string) => void
145
+ }) {
146
+ onProgress('Submitting to EBI phmmer...')
147
+ const jobId = await submitEbiJob({
148
+ tool: TOOL,
149
+ params: {
150
+ database,
151
+ sequence: query,
152
+ // the alignment is the whole point of using phmmer here
153
+ alignView: 'true',
154
+ },
155
+ })
156
+ onRid(jobId)
157
+
158
+ await waitForEbiJob({
159
+ tool: TOOL,
160
+ jobId,
161
+ onCountdown: s => {
162
+ onProgress(`Re-checking phmmer status in... ${s}`)
163
+ },
164
+ })
165
+
166
+ const alignment = parsePhmmerAlignment({
167
+ stockholm: await fetchEbiResult({ tool: TOOL, jobId, type: 'sto' }),
168
+ query,
169
+ })
170
+ if (alignment.rows.length === 0) {
171
+ throw new Error('No hits found')
172
+ }
173
+ return { rid: jobId, ...alignment }
174
+ }
@@ -51,8 +51,13 @@ export interface TaxonomyInfo {
51
51
  }
52
52
 
53
53
  export async function fetchTaxonomyInfo(
54
- taxids: number[],
54
+ taxidsWithRepeats: number[],
55
55
  ): Promise<Map<number, TaxonomyInfo>> {
56
+ // callers pass one taxid per alignment row, and a BLAST hit list is several
57
+ // rows per species: 100 albumin hits are maybe 50 taxa, and asking as they
58
+ // came did 100 IndexedDB reads and sent eutils 100 ids for 50 answers. The
59
+ // result is keyed by taxid, so no caller can tell the difference
60
+ const taxids = [...new Set(taxidsWithRepeats)]
56
61
  const result = new Map<number, TaxonomyInfo>()
57
62
  const uncachedTaxids: number[] = []
58
63
  const cachedResults = await getCachedTaxonomies(taxids)
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '3.3.0'
1
+ export const version = '3.4.0'
@@ -1,6 +0,0 @@
1
- import React from 'react';
2
- import type { JBrowsePluginMsaViewModel } from '../model';
3
- declare const LoadingBLAST: ({ model, }: {
4
- model: JBrowsePluginMsaViewModel;
5
- }) => React.JSX.Element;
6
- export default LoadingBLAST;
@@ -1,26 +0,0 @@
1
- import React from 'react';
2
- import { ErrorMessage, LoadingEllipses } from '@jbrowse/core/ui';
3
- import { Typography } from '@mui/material';
4
- import { observer } from 'mobx-react';
5
- import { makeStyles } from 'tss-react/mui';
6
- import JobLink from './JobLink';
7
- const useStyles = makeStyles()(theme => ({
8
- margin: {
9
- padding: 20,
10
- },
11
- loading: {
12
- background: theme.palette.background.paper,
13
- },
14
- }));
15
- const LoadingBLAST = observer(function LoadingBLAST2({ model, }) {
16
- const { progress, rid, error } = model;
17
- const { classes } = useStyles();
18
- return (React.createElement("div", { className: classes.margin },
19
- React.createElement(LoadingEllipses, { message: "Running EBI BLAST", variant: "h5" }),
20
- error ? (React.createElement("div", null,
21
- rid ? React.createElement(JobLink, { jobId: rid }) : null,
22
- React.createElement(ErrorMessage, { error: error }))) : rid ? (React.createElement("div", { className: classes.loading },
23
- React.createElement(JobLink, { jobId: rid }),
24
- React.createElement(Typography, null, progress))) : (React.createElement(Typography, null, progress || 'Initializing BLAST query'))));
25
- });
26
- export default LoadingBLAST;
@@ -1,48 +0,0 @@
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()(theme => ({
13
- margin: {
14
- padding: 20,
15
- },
16
- loading: {
17
- background: theme.palette.background.paper,
18
- },
19
- }))
20
-
21
- const LoadingBLAST = observer(function LoadingBLAST2({
22
- model,
23
- }: {
24
- model: JBrowsePluginMsaViewModel
25
- }) {
26
- const { progress, rid, error } = model
27
- const { classes } = useStyles()
28
- return (
29
- <div className={classes.margin}>
30
- <LoadingEllipses message="Running EBI BLAST" variant="h5" />
31
- {error ? (
32
- <div>
33
- {rid ? <JobLink jobId={rid} /> : null}
34
- <ErrorMessage error={error} />
35
- </div>
36
- ) : rid ? (
37
- <div className={classes.loading}>
38
- <JobLink jobId={rid} />
39
- <Typography>{progress}</Typography>
40
- </div>
41
- ) : (
42
- <Typography>{progress || 'Initializing BLAST query'}</Typography>
43
- )}
44
- </div>
45
- )
46
- })
47
-
48
- export default LoadingBLAST