jbrowse-plugin-msaview 2.7.1 → 2.7.3

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 (59) hide show
  1. package/dist/AddHighlightModel/MsaToGenomeHighlight.js +6 -5
  2. package/dist/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.js +1 -1
  3. package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastPanel.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/calculateProteinSequence.d.ts +3 -2
  7. package/dist/LaunchMsaView/components/calculateProteinSequence.js +31 -15
  8. package/dist/LaunchMsaView/components/geneticCodes.d.ts +15 -0
  9. package/dist/LaunchMsaView/components/geneticCodes.js +227 -0
  10. package/dist/LaunchMsaView/components/types.d.ts +1 -0
  11. package/dist/LaunchMsaView/index.js +32 -26
  12. package/dist/MsaViewPanel/doLaunchBlast.js +10 -22
  13. package/dist/MsaViewPanel/genomeToMSA.js +13 -6
  14. package/dist/MsaViewPanel/genomeToMSA.test.js +35 -6
  15. package/dist/MsaViewPanel/model.d.ts +68 -66
  16. package/dist/MsaViewPanel/model.js +14 -12
  17. package/dist/MsaViewPanel/msaCoordToGenomeCoord.d.ts +32 -13
  18. package/dist/MsaViewPanel/msaCoordToGenomeCoord.js +43 -14
  19. package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.js +108 -18
  20. package/dist/MsaViewPanel/msaDataStore.js +8 -17
  21. package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +8 -6
  22. package/dist/jbrowse-plugin-msaview.umd.production.min.js +31 -31
  23. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  24. package/dist/utils/blastCache.d.ts +1 -2
  25. package/dist/utils/blastCache.js +9 -22
  26. package/dist/utils/domainCache.js +6 -15
  27. package/dist/utils/idb.d.ts +12 -0
  28. package/dist/utils/idb.js +21 -0
  29. package/dist/utils/taxonomyNames.js +13 -18
  30. package/dist/utils/useLocalStorage.d.ts +1 -0
  31. package/dist/utils/useLocalStorage.js +29 -0
  32. package/dist/version.d.ts +1 -1
  33. package/dist/version.js +1 -1
  34. package/package.json +5 -3
  35. package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +6 -8
  36. package/src/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.tsx +1 -1
  37. package/src/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastPanel.tsx +1 -1
  38. package/src/LaunchMsaView/components/NCBIBlastQuery/useCachedBlastResults.ts +4 -2
  39. package/src/LaunchMsaView/components/calculateProteinSequence.ts +35 -20
  40. package/src/LaunchMsaView/components/geneticCodes.ts +298 -0
  41. package/src/LaunchMsaView/components/types.ts +3 -0
  42. package/src/LaunchMsaView/index.ts +44 -35
  43. package/src/MsaViewPanel/doLaunchBlast.ts +20 -29
  44. package/src/MsaViewPanel/genomeToMSA.test.ts +38 -6
  45. package/src/MsaViewPanel/genomeToMSA.ts +14 -6
  46. package/src/MsaViewPanel/model.ts +17 -12
  47. package/src/MsaViewPanel/msaCoordToGenomeCoord.test.ts +117 -18
  48. package/src/MsaViewPanel/msaCoordToGenomeCoord.ts +70 -26
  49. package/src/MsaViewPanel/msaDataStore.ts +17 -17
  50. package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +8 -6
  51. package/src/utils/blastCache.ts +20 -23
  52. package/src/utils/domainCache.ts +14 -18
  53. package/src/utils/idb.ts +28 -0
  54. package/src/utils/taxonomyNames.ts +22 -24
  55. package/src/utils/useLocalStorage.ts +31 -0
  56. package/src/version.ts +1 -1
  57. package/dist/MsaViewPanel/blosum62.d.ts +0 -2
  58. package/dist/MsaViewPanel/blosum62.js +0 -627
  59. package/src/MsaViewPanel/blosum62.ts +0 -628
@@ -0,0 +1,298 @@
1
+ // NCBI translation tables (genetic codes), copied verbatim from
2
+ // jbrowse-components packages/core/src/util/geneticCodes.ts, which parses them
3
+ // from NCBI's authoritative gc.prt. `ncbieaa` gives the amino acid for each of
4
+ // the 64 codons in a fixed base order; `sncbieaa` marks valid start codons.
5
+ //
6
+ // Vendored rather than imported: core exports this only on main, and the msaview
7
+ // bundle has to translate the same way on every host a config names, back to
8
+ // v4.0.0. Replace the whole file with a re-export of
9
+ // `@jbrowse/core/util/geneticCodes` once a release ships it -- that path is not
10
+ // in ReExports, so it bundles rather than binding to the host.
11
+ export interface NcbiGeneticCode {
12
+ id: number
13
+ name: string
14
+ ncbieaa: string
15
+ sncbieaa: string
16
+ }
17
+
18
+ export const ncbiGeneticCodes: NcbiGeneticCode[] = [
19
+ {
20
+ id: 1,
21
+ name: 'Standard',
22
+ ncbieaa: 'FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
23
+ sncbieaa:
24
+ '---M------**--*----M---------------M----------------------------',
25
+ },
26
+ {
27
+ id: 2,
28
+ name: 'Vertebrate Mitochondrial',
29
+ ncbieaa: 'FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIMMTTTTNNKKSS**VVVVAAAADDEEGGGG',
30
+ sncbieaa:
31
+ '----------**--------------------MMMM----------**---M------------',
32
+ },
33
+ {
34
+ id: 3,
35
+ name: 'Yeast Mitochondrial',
36
+ ncbieaa: 'FFLLSSSSYY**CCWWTTTTPPPPHHQQRRRRIIMMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
37
+ sncbieaa:
38
+ '----------**----------------------MM---------------M------------',
39
+ },
40
+ {
41
+ id: 4,
42
+ name: 'Mold Mitochondrial; Protozoan Mitochondrial; Coelenterate Mitochondrial; Mycoplasma; Spiroplasma',
43
+ ncbieaa: 'FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
44
+ sncbieaa:
45
+ '--MM------**-------M------------MMMM---------------M------------',
46
+ },
47
+ {
48
+ id: 5,
49
+ name: 'Invertebrate Mitochondrial',
50
+ ncbieaa: 'FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIMMTTTTNNKKSSSSVVVVAAAADDEEGGGG',
51
+ sncbieaa:
52
+ '---M------**--------------------MMMM---------------M------------',
53
+ },
54
+ {
55
+ id: 6,
56
+ name: 'Ciliate Nuclear; Dasycladacean Nuclear; Hexamita Nuclear',
57
+ ncbieaa: 'FFLLSSSSYYQQCC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
58
+ sncbieaa:
59
+ '--------------*--------------------M----------------------------',
60
+ },
61
+ {
62
+ id: 9,
63
+ name: 'Echinoderm Mitochondrial; Flatworm Mitochondrial',
64
+ ncbieaa: 'FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIIMTTTTNNNKSSSSVVVVAAAADDEEGGGG',
65
+ sncbieaa:
66
+ '----------**-----------------------M---------------M------------',
67
+ },
68
+ {
69
+ id: 10,
70
+ name: 'Euplotid Nuclear',
71
+ ncbieaa: 'FFLLSSSSYY**CCCWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
72
+ sncbieaa:
73
+ '----------**-----------------------M----------------------------',
74
+ },
75
+ {
76
+ id: 11,
77
+ name: 'Bacterial, Archaeal and Plant Plastid',
78
+ ncbieaa: 'FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
79
+ sncbieaa:
80
+ '---M------**--*----M------------MMMM---------------M------------',
81
+ },
82
+ {
83
+ id: 12,
84
+ name: 'Alternative Yeast Nuclear',
85
+ ncbieaa: 'FFLLSSSSYY**CC*WLLLSPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
86
+ sncbieaa:
87
+ '----------**--*----M---------------M----------------------------',
88
+ },
89
+ {
90
+ id: 13,
91
+ name: 'Ascidian Mitochondrial',
92
+ ncbieaa: 'FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIMMTTTTNNKKSSGGVVVVAAAADDEEGGGG',
93
+ sncbieaa:
94
+ '---M------**----------------------MM---------------M------------',
95
+ },
96
+ {
97
+ id: 14,
98
+ name: 'Alternative Flatworm Mitochondrial',
99
+ ncbieaa: 'FFLLSSSSYYY*CCWWLLLLPPPPHHQQRRRRIIIMTTTTNNNKSSSSVVVVAAAADDEEGGGG',
100
+ sncbieaa:
101
+ '-----------*-----------------------M----------------------------',
102
+ },
103
+ {
104
+ id: 15,
105
+ name: 'Blepharisma Macronuclear',
106
+ ncbieaa: 'FFLLSSSSYY*QCC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
107
+ sncbieaa:
108
+ '----------*---*--------------------M----------------------------',
109
+ },
110
+ {
111
+ id: 16,
112
+ name: 'Chlorophycean Mitochondrial',
113
+ ncbieaa: 'FFLLSSSSYY*LCC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
114
+ sncbieaa:
115
+ '----------*---*--------------------M----------------------------',
116
+ },
117
+ {
118
+ id: 21,
119
+ name: 'Trematode Mitochondrial',
120
+ ncbieaa: 'FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIMMTTTTNNNKSSSSVVVVAAAADDEEGGGG',
121
+ sncbieaa:
122
+ '----------**-----------------------M---------------M------------',
123
+ },
124
+ {
125
+ id: 22,
126
+ name: 'Scenedesmus obliquus Mitochondrial',
127
+ ncbieaa: 'FFLLSS*SYY*LCC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
128
+ sncbieaa:
129
+ '------*---*---*--------------------M----------------------------',
130
+ },
131
+ {
132
+ id: 23,
133
+ name: 'Thraustochytrium Mitochondrial',
134
+ ncbieaa: 'FF*LSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
135
+ sncbieaa:
136
+ '--*-------**--*-----------------M--M---------------M------------',
137
+ },
138
+ {
139
+ id: 24,
140
+ name: 'Rhabdopleuridae Mitochondrial',
141
+ ncbieaa: 'FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSSKVVVVAAAADDEEGGGG',
142
+ sncbieaa:
143
+ '---M------**-------M---------------M---------------M------------',
144
+ },
145
+ {
146
+ id: 25,
147
+ name: 'Candidate Division SR1 and Gracilibacteria',
148
+ ncbieaa: 'FFLLSSSSYY**CCGWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
149
+ sncbieaa:
150
+ '---M------**-----------------------M---------------M------------',
151
+ },
152
+ {
153
+ id: 26,
154
+ name: 'Pachysolen tannophilus Nuclear',
155
+ ncbieaa: 'FFLLSSSSYY**CC*WLLLAPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
156
+ sncbieaa:
157
+ '----------**--*----M---------------M----------------------------',
158
+ },
159
+ {
160
+ id: 27,
161
+ name: 'Karyorelict Nuclear',
162
+ ncbieaa: 'FFLLSSSSYYQQCCWWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
163
+ sncbieaa:
164
+ '--------------*--------------------M----------------------------',
165
+ },
166
+ {
167
+ id: 28,
168
+ name: 'Condylostoma Nuclear',
169
+ ncbieaa: 'FFLLSSSSYYQQCCWWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
170
+ sncbieaa:
171
+ '----------**--*--------------------M----------------------------',
172
+ },
173
+ {
174
+ id: 29,
175
+ name: 'Mesodinium Nuclear',
176
+ ncbieaa: 'FFLLSSSSYYYYCC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
177
+ sncbieaa:
178
+ '--------------*--------------------M----------------------------',
179
+ },
180
+ {
181
+ id: 30,
182
+ name: 'Peritrich Nuclear',
183
+ ncbieaa: 'FFLLSSSSYYEECC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
184
+ sncbieaa:
185
+ '--------------*--------------------M----------------------------',
186
+ },
187
+ {
188
+ id: 31,
189
+ name: 'Blastocrithidia Nuclear',
190
+ ncbieaa: 'FFLLSSSSYYEECCWWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
191
+ sncbieaa:
192
+ '----------**-----------------------M----------------------------',
193
+ },
194
+ {
195
+ id: 32,
196
+ name: 'Balanophoraceae Plastid',
197
+ ncbieaa: 'FFLLSSSSYY*WCC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
198
+ sncbieaa:
199
+ '---M------*---*----M------------MMMM---------------M------------',
200
+ },
201
+ {
202
+ id: 33,
203
+ name: 'Cephalodiscidae Mitochondrial',
204
+ ncbieaa: 'FFLLSSSSYYY*CCWWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSSKVVVVAAAADDEEGGGG',
205
+ sncbieaa:
206
+ '---M-------*-------M---------------M---------------M------------',
207
+ },
208
+ ]
209
+
210
+ // The codon order shared by every NCBI table -- the Base1/Base2/Base3 comment
211
+ // rows in gc.prt. codon i = BASE1[i] + BASE2[i] + BASE3[i].
212
+ const BASE1 = 'TTTTTTTTTTTTTTTTCCCCCCCCCCCCCCCCAAAAAAAAAAAAAAAAGGGGGGGGGGGGGGGG'
213
+ const BASE2 = 'TTTTCCCCAAAAGGGGTTTTCCCCAAAAGGGGTTTTCCCCAAAAGGGGTTTTCCCCAAAAGGGG'
214
+ const BASE3 = 'TCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAG'
215
+
216
+ const CODONS = Array.from(
217
+ { length: 64 },
218
+ (_, i) => BASE1[i]! + BASE2[i]! + BASE3[i]!,
219
+ )
220
+
221
+ const ncbiCodeById = new Map(ncbiGeneticCodes.map(t => [t.id, t]))
222
+
223
+ export interface GeneticCode {
224
+ id: number
225
+ name: string
226
+ // case-insensitive codon -> amino acid letter, '*' for a stop codon
227
+ codonTable: Record<string, string>
228
+ // valid start codons (uppercase)
229
+ starts: string[]
230
+ }
231
+
232
+ // Expand an uppercase codon map so every case combination of a triplet resolves,
233
+ // which is what callers reading raw sequence need.
234
+ function caseExpand(table: Record<string, string>) {
235
+ const out: Record<string, string> = {}
236
+ for (const [codon, aa] of Object.entries(table)) {
237
+ const cases = (i: number) => {
238
+ const n = codon.charAt(i)
239
+ return [n.toUpperCase(), n.toLowerCase()]
240
+ }
241
+ for (const n0 of cases(0)) {
242
+ for (const n1 of cases(1)) {
243
+ for (const n2 of cases(2)) {
244
+ out[n0 + n1 + n2] = aa
245
+ }
246
+ }
247
+ }
248
+ }
249
+ return out
250
+ }
251
+
252
+ function buildGeneticCode(id: number): GeneticCode {
253
+ const def = ncbiCodeById.get(id)
254
+ if (!def && id !== 1) {
255
+ console.warn(
256
+ `Unknown genetic code (transl_table=${id}); using standard code`,
257
+ )
258
+ }
259
+ const {
260
+ id: resolvedId,
261
+ name,
262
+ ncbieaa,
263
+ sncbieaa,
264
+ } = def ?? ncbiCodeById.get(1)!
265
+ const table: Record<string, string> = {}
266
+ const starts: string[] = []
267
+ for (const [i, codon] of CODONS.entries()) {
268
+ table[codon] = ncbieaa[i]!
269
+ if (sncbieaa[i] === 'M') {
270
+ starts.push(codon)
271
+ }
272
+ }
273
+ return { id: resolvedId, name, codonTable: caseExpand(table), starts }
274
+ }
275
+
276
+ const geneticCodeCache = new Map<number, GeneticCode>()
277
+
278
+ // Resolves the codon map + start set for an NCBI translation-table id, falling
279
+ // back to the standard code (1) for an unrecognized id. Memoized: there are only
280
+ // ~27 tables and each result is immutable.
281
+ export function getGeneticCode(id = 1): GeneticCode {
282
+ let code = geneticCodeCache.get(id)
283
+ if (!code) {
284
+ code = buildGeneticCode(id)
285
+ geneticCodeCache.set(id, code)
286
+ }
287
+ return code
288
+ }
289
+
290
+ // Parses a GFF/GenBank `transl_table` attribute value into an NCBI table id. The
291
+ // GFF adapter yields a string (or an array if the attribute repeated), so this
292
+ // normalizes both; returns undefined for a missing or non-positive-integer value
293
+ // so callers fall back to their default code.
294
+ export function parseTranslTable(value: unknown): number | undefined {
295
+ const raw = Array.isArray(value) ? value[0] : value
296
+ const n = Number(raw)
297
+ return Number.isInteger(n) && n > 0 ? n : undefined
298
+ }
@@ -2,6 +2,9 @@ export interface Feat {
2
2
  start: number
3
3
  end: number
4
4
  type?: string
5
+ // GFF phase of the first coding base; convertCodingSequenceToPeptides reads it
6
+ // off cds[0] to start translation in the right frame
7
+ phase?: number
5
8
  }
6
9
 
7
10
  export interface SeqState {
@@ -15,8 +15,9 @@ function isDisplay(elt: { name: string }): elt is DisplayType {
15
15
  }
16
16
 
17
17
  // The canvas LinearBasicDisplay (JBrowse >=4.3) exposes the right-clicked
18
- // feature via contextMenuInfo + async fetchFullFeature rather than a synchronous
19
- // feature object.
18
+ // feature via contextMenuInfo + async fetchFullFeature. Hosts before that -- and
19
+ // the v3.7.0 in the wild that shipped configs still name -- expose it
20
+ // synchronously as contextMenuFeature, and only have that one.
20
21
  interface ContextMenuInfo {
21
22
  item: { featureId: string; type?: string }
22
23
  displayedRegionIndex: number
@@ -25,13 +26,16 @@ interface ContextMenuInfo {
25
26
  interface DisplayModel {
26
27
  contextMenuItems: () => MenuItem[]
27
28
  contextMenuInfo?: ContextMenuInfo
28
- isGeneLike: boolean
29
- fetchFullFeature: (
29
+ isGeneLike?: boolean
30
+ fetchFullFeature?: (
30
31
  featureId: string,
31
32
  displayedRegionIndex: number,
32
33
  ) => Promise<Feature | undefined>
34
+ contextMenuFeature?: Feature
33
35
  }
34
36
 
37
+ const GENE_LIKE_TYPES = new Set(['gene', 'mRNA', 'transcript'])
38
+
35
39
  function extendStateModel(stateModel: IAnyModelType) {
36
40
  return stateModel.views((self: DisplayModel) => {
37
41
  const superContextMenuItems = self.contextMenuItems
@@ -39,40 +43,45 @@ function extendStateModel(stateModel: IAnyModelType) {
39
43
  contextMenuItems() {
40
44
  const track = getContainingTrack(self)
41
45
  const session = getSession(track)
46
+ const launch = (feature: Feature) => {
47
+ session.queueDialog(handleClose => [
48
+ LaunchMsaViewDialog,
49
+ { model: track, handleClose, feature },
50
+ ])
51
+ }
52
+
42
53
  const info = self.contextMenuInfo
43
- const showMsaMenuItem = info && self.isGeneLike
54
+ const fetchFullFeature = self.fetchFullFeature
55
+ const legacyFeature = self.contextMenuFeature
56
+ const onClick =
57
+ info && fetchFullFeature && self.isGeneLike
58
+ ? () => {
59
+ fetchFullFeature(info.item.featureId, info.displayedRegionIndex)
60
+ .then(feature => {
61
+ if (feature) {
62
+ launch(feature)
63
+ } else {
64
+ session.notify(
65
+ 'Could not load feature for MSA view',
66
+ 'warning',
67
+ )
68
+ }
69
+ })
70
+ .catch((e: unknown) => {
71
+ session.notifyError(`${e}`, e)
72
+ })
73
+ }
74
+ : legacyFeature &&
75
+ GENE_LIKE_TYPES.has(String(legacyFeature.get('type')))
76
+ ? () => {
77
+ launch(legacyFeature)
78
+ }
79
+ : undefined
80
+
44
81
  return [
45
82
  ...superContextMenuItems(),
46
- ...(showMsaMenuItem
47
- ? [
48
- {
49
- label: 'Launch MSA view',
50
- icon: AddIcon,
51
- onClick: () => {
52
- self
53
- .fetchFullFeature(
54
- info.item.featureId,
55
- info.displayedRegionIndex,
56
- )
57
- .then(feature => {
58
- if (feature) {
59
- session.queueDialog(handleClose => [
60
- LaunchMsaViewDialog,
61
- { model: track, handleClose, feature },
62
- ])
63
- } else {
64
- session.notify(
65
- 'Could not load feature for MSA view',
66
- 'warning',
67
- )
68
- }
69
- })
70
- .catch((e: unknown) => {
71
- session.notifyError(`${e}`, e)
72
- })
73
- },
74
- },
75
- ]
83
+ ...(onClick
84
+ ? [{ label: 'Launch MSA view', icon: AddIcon, onClick }]
76
85
  : []),
77
86
  ]
78
87
  },
@@ -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)
@@ -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
  }
@@ -19,7 +19,10 @@ import {
19
19
  storeDataToIndexedDB,
20
20
  syncGenomeHoverToMsaColumn,
21
21
  } from './afterCreateAutoruns'
22
- import { msaCoordToGenomeCoord } from './msaCoordToGenomeCoord'
22
+ import {
23
+ msaCoordToGenomeCoord,
24
+ msaCoordToGenomeRegions,
25
+ } from './msaCoordToGenomeCoord'
23
26
 
24
27
  import type { MafRegion, MsaViewInitState } from './types'
25
28
  import type {
@@ -188,35 +191,37 @@ export default function stateModelFactory() {
188
191
  .views(self => ({
189
192
  /**
190
193
  * #getter
191
- * Genome region under the current MSA hover column. Suppressed on the LGV
194
+ * Genome regions under the current MSA hover column. Suppressed on the LGV
192
195
  * while it's being hovered (GenomeMouseoverHighlight shows the crisp 1bp
193
196
  * marker there instead of this wider codon band).
194
197
  */
195
- get connectedHoverHighlight(): IRegion | undefined {
198
+ get connectedHoverHighlights(): IRegion[] {
196
199
  const { mouseCol } = self
197
200
  return mouseCol === undefined
198
- ? undefined
199
- : msaCoordToGenomeCoord({ model: self, coord: mouseCol })
201
+ ? []
202
+ : msaCoordToGenomeRegions({ model: self, coord: mouseCol })
200
203
  },
201
204
  /**
202
205
  * #getter
203
- * Genome region under the persistent MSA click selection. Shown
206
+ * Genome regions under the persistent MSA click selection. Shown
204
207
  * regardless of LGV hover, so hovering the genome doesn't hide it.
205
208
  */
206
- get connectedClickHighlight(): IRegion | undefined {
209
+ get connectedClickHighlights(): IRegion[] {
207
210
  const { mouseClickCol } = self
208
211
  return mouseClickCol === undefined
209
- ? undefined
210
- : msaCoordToGenomeCoord({ model: self, coord: mouseClickCol })
212
+ ? []
213
+ : msaCoordToGenomeRegions({ model: self, coord: mouseClickCol })
211
214
  },
212
215
  /**
213
216
  * #getter
217
+ * cross-plugin contract: jbrowse-plugin-mafviewer reads this off the view
218
+ * to draw the same highlights in its own display
214
219
  */
215
220
  get connectedHighlights(): IRegion[] {
216
221
  return [
217
- this.connectedHoverHighlight,
218
- this.connectedClickHighlight,
219
- ].filter((r): r is IRegion => r !== undefined)
222
+ ...this.connectedHoverHighlights,
223
+ ...this.connectedClickHighlights,
224
+ ]
220
225
  },
221
226
  }))
222
227