jbrowse-plugin-msaview 2.5.1 → 2.6.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.
@@ -73,7 +73,9 @@ function parseFeature(featureXml) {
73
73
  : noteName || quals.site_type || 'site';
74
74
  const accession = isDomain ? cddId : `${cddId}:${name}`;
75
75
  return {
76
- signature: { entry: { name, description: quals.note ?? name, accession } },
76
+ signature: {
77
+ entry: { name, description: quals.note ?? name, accession },
78
+ },
77
79
  locations: [span],
78
80
  };
79
81
  }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "2.5.1";
1
+ export declare const version = "2.6.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = '2.5.1';
1
+ export const version = '2.6.0';
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.5.1",
2
+ "version": "2.6.0",
3
3
  "license": "MIT",
4
4
  "name": "jbrowse-plugin-msaview",
5
5
  "repository": {
@@ -17,41 +17,42 @@
17
17
  ],
18
18
  "dependencies": {
19
19
  "@emotion/styled": "^11.14.1",
20
+ "@gmod/tabix": "^3.0.1",
20
21
  "g2p_mapper": "^2.1.5",
21
22
  "idb": "^8.0.3",
22
23
  "pako-esm2": "^2.0.2",
23
24
  "react-msaview": "^5.4.1",
24
- "swr": "^2.4.1"
25
+ "swr": "^2.4.2"
25
26
  },
26
27
  "devDependencies": {
27
28
  "@emotion/react": "^11.14.0",
28
29
  "@eslint/js": "^10.0.1",
29
30
  "@fal-works/esbuild-plugin-global-externals": "^2.1.2",
30
31
  "@jbrowse/core": "^4.3.0",
31
- "@jbrowse/mobx-state-tree": "^5.10.8",
32
+ "@jbrowse/mobx-state-tree": "^5.11.1",
32
33
  "@jbrowse/plugin-linear-genome-view": "^4.3.0",
33
34
  "@mui/icons-material": "^7.3.11",
34
35
  "@mui/material": "^7.3.11",
35
36
  "@mui/system": "^7.3.11",
36
- "@mui/x-data-grid": "^8.28.7",
37
- "@types/node": "^25.9.1",
38
- "@types/react": "^19.2.16",
39
- "@typescript-eslint/eslint-plugin": "^8.60.1",
40
- "@typescript-eslint/parser": "^8.60.1",
41
- "esbuild": "^0.28.0",
42
- "eslint": "^10.4.1",
43
- "eslint-plugin-import-x": "^4.16.2",
37
+ "@mui/x-data-grid": "^8.29.1",
38
+ "@types/node": "^25.9.4",
39
+ "@types/react": "^19.2.17",
40
+ "@typescript-eslint/eslint-plugin": "^8.62.0",
41
+ "@typescript-eslint/parser": "^8.62.0",
42
+ "esbuild": "^0.28.1",
43
+ "eslint": "^10.5.0",
44
+ "eslint-plugin-import-x": "^4.17.0",
44
45
  "eslint-plugin-react": "^7.37.5",
45
46
  "eslint-plugin-react-hooks": "^7.1.1",
46
47
  "eslint-plugin-unicorn": "^64.0.0",
47
- "mobx": "^6.15.4",
48
+ "mobx": "^6.16.1",
48
49
  "mobx-react": "^9.2.2",
49
50
  "msa-parsers": "5.4.1",
50
51
  "pixelmatch": "^7.2.0",
51
52
  "pngjs": "^7.0.0",
52
- "prettier": "^3.8.3",
53
+ "prettier": "^3.8.4",
53
54
  "pretty-bytes": "^7.1.0",
54
- "puppeteer": "^25.1.0",
55
+ "puppeteer": "^25.2.1",
55
56
  "react": "^19.2.7",
56
57
  "react-dom": "^19.2.7",
57
58
  "rimraf": "^6.1.3",
@@ -59,8 +60,8 @@
59
60
  "serve": "^14.2.6",
60
61
  "tss-react": "^4.9.21",
61
62
  "typescript": "^6.0.3",
62
- "typescript-eslint": "^8.60.1",
63
- "vitest": "^4.1.8"
63
+ "typescript-eslint": "^8.62.0",
64
+ "vitest": "^4.1.9"
64
65
  },
65
66
  "scripts": {
66
67
  "clean": "rimraf dist",
@@ -11,6 +11,9 @@ export default function LaunchMsaViewExtensionPointF(
11
11
  session,
12
12
  data,
13
13
  msaFileLocation,
14
+ msaTabixLocation,
15
+ msaIndexLocation,
16
+ msaId,
14
17
  treeFileLocation,
15
18
  connectedViewId,
16
19
  connectedFeature,
@@ -29,6 +32,9 @@ export default function LaunchMsaViewExtensionPointF(
29
32
  session: AbstractSessionModel
30
33
  data?: { msa: string; tree?: string }
31
34
  msaFileLocation?: { uri: string }
35
+ msaTabixLocation?: { uri: string }
36
+ msaIndexLocation?: { uri: string }
37
+ msaId?: string
32
38
  treeFileLocation?: { uri: string }
33
39
  connectedViewId?: string
34
40
  connectedFeature?: Record<string, unknown>
@@ -44,7 +50,7 @@ export default function LaunchMsaViewExtensionPointF(
44
50
  querySeqName?: string
45
51
  highlightColumns?: number[]
46
52
  }) => {
47
- if (!data && !msaFileLocation) {
53
+ if (!data && !msaFileLocation && !msaTabixLocation) {
48
54
  throw new Error(
49
55
  'No MSA data or file location provided when launching MSA view',
50
56
  )
@@ -68,6 +74,9 @@ export default function LaunchMsaViewExtensionPointF(
68
74
  msaData: data?.msa,
69
75
  treeData: data?.tree,
70
76
  msaUrl: msaFileLocation?.uri,
77
+ msaTabixLocation,
78
+ msaIndexLocation,
79
+ msaId,
71
80
  treeUrl: treeFileLocation?.uri,
72
81
  querySeqName,
73
82
  },
@@ -1,6 +1,7 @@
1
1
  import { getSession } from '@jbrowse/core/util'
2
2
 
3
3
  import { doLaunchBlast } from './doLaunchBlast'
4
+ import { fetchTabixMsa } from './fetchTabixMsa'
4
5
  import { genomeToMSA } from './genomeToMSA'
5
6
  import { loadProteinDomains } from './loadProteinDomains'
6
7
  import {
@@ -133,7 +134,16 @@ export function processInit(self: JBrowsePluginMsaViewModel) {
133
134
  void (async () => {
134
135
  try {
135
136
  self.setError(undefined)
136
- const { msaData, msaUrl, treeData, treeUrl, querySeqName } = init
137
+ const {
138
+ msaData,
139
+ msaUrl,
140
+ msaTabixLocation,
141
+ msaIndexLocation,
142
+ msaId,
143
+ treeData,
144
+ treeUrl,
145
+ querySeqName,
146
+ } = init
137
147
 
138
148
  if (msaUrl) {
139
149
  const id = getUniprotIdFromAlphaFoldUrl(msaUrl)
@@ -156,6 +166,25 @@ export function processInit(self: JBrowsePluginMsaViewModel) {
156
166
  }
157
167
  const data = await response.text()
158
168
  self.setMSA(data)
169
+ } else if (msaTabixLocation) {
170
+ const feature = self.connectedFeature
171
+ if (feature) {
172
+ const fasta = await fetchTabixMsa({
173
+ location: msaTabixLocation,
174
+ indexLocation: msaIndexLocation,
175
+ msaId: msaId ?? String(feature.name),
176
+ refName: String(feature.refName),
177
+ start: Number(feature.start),
178
+ end: Number(feature.end),
179
+ })
180
+ if (fasta) {
181
+ self.setMSA(fasta)
182
+ } else {
183
+ throw new Error(
184
+ `No alignment for ${msaId ?? String(feature.name)} in ${msaTabixLocation.uri}`,
185
+ )
186
+ }
187
+ }
159
188
  }
160
189
 
161
190
  if (treeData) {
@@ -282,46 +311,71 @@ export function autoConnectStructures(self: JBrowsePluginMsaViewModel) {
282
311
  }
283
312
  }
284
313
 
314
+ /**
315
+ * Mirror a connected 3D protein view's hovered residue onto the MSA's
316
+ * highlighted columns. Returns the autorun body and keeps a flag tracking
317
+ * whether the current highlight was set by THIS sync: when a protein hover ends
318
+ * we restore the declarative highlightColumns seed (or clear) rather than
319
+ * blindly wiping it.
320
+ *
321
+ * Without the flag this autorun fires once on creation — with the view connected
322
+ * to a *genome* LGV but no 3D protein structure attached — computes zero columns,
323
+ * and calls setHighlightedColumns(undefined), clobbering the seed that
324
+ * MSAModelF.afterCreate just set from the declarative `highlightColumns`. That is
325
+ * the bug that made the BRAF/TP53 genome-browser links open with no V600/R248
326
+ * column lit (SRC has no highlightColumns, so nothing was there to wipe).
327
+ */
285
328
  export function observeProteinHighlights(self: JBrowsePluginMsaViewModel) {
286
- const { connectedViewId, transcriptToMsaMap, querySeqName } = self
329
+ let proteinDriven = false
330
+ return () => {
331
+ const { connectedViewId, transcriptToMsaMap, querySeqName } = self
287
332
 
288
- if (!connectedViewId || !transcriptToMsaMap) {
289
- return
290
- }
333
+ if (!connectedViewId || !transcriptToMsaMap) {
334
+ return
335
+ }
291
336
 
292
- const columns = new Set<number>()
337
+ const columns = new Set<number>()
293
338
 
294
- for (const view of getProteinViews(getSession(self).views)) {
295
- for (const structure of view.structures) {
296
- if (structure.connectedViewId !== connectedViewId) {
297
- continue
298
- }
339
+ for (const view of getProteinViews(getSession(self).views)) {
340
+ for (const structure of view.structures) {
341
+ if (structure.connectedViewId !== connectedViewId) {
342
+ continue
343
+ }
299
344
 
300
- const highlights = structure.hoverGenomeHighlights
301
- if (!highlights || highlights.length === 0) {
302
- continue
303
- }
345
+ const highlights = structure.hoverGenomeHighlights
346
+ if (!highlights || highlights.length === 0) {
347
+ continue
348
+ }
304
349
 
305
- const { g2p } = transcriptToMsaMap
306
- for (const highlight of highlights) {
307
- for (let coord = highlight.start; coord < highlight.end; coord++) {
308
- const proteinPos = g2p[coord]
309
- if (proteinPos !== undefined) {
310
- const col = self.seqPosToGlobalCol(querySeqName, proteinPos)
311
- columns.add(col)
350
+ const { g2p } = transcriptToMsaMap
351
+ for (const highlight of highlights) {
352
+ for (let coord = highlight.start; coord < highlight.end; coord++) {
353
+ const proteinPos = g2p[coord]
354
+ if (proteinPos !== undefined) {
355
+ const col = self.seqPosToGlobalCol(querySeqName, proteinPos)
356
+ columns.add(col)
357
+ }
312
358
  }
313
359
  }
314
360
  }
315
361
  }
316
- }
317
-
318
- const visibleColumns = Array.from(columns)
319
- .map(col => self.globalColToVisibleCol(col))
320
- .filter((col): col is number => col !== undefined)
321
362
 
322
- self.setHighlightedColumns(
323
- visibleColumns.length > 0 ? visibleColumns : undefined,
324
- )
363
+ const visibleColumns = Array.from(columns)
364
+ .map(col => self.globalColToVisibleCol(col))
365
+ .filter((col): col is number => col !== undefined)
366
+
367
+ if (visibleColumns.length > 0) {
368
+ self.setHighlightedColumns(visibleColumns)
369
+ proteinDriven = true
370
+ } else if (proteinDriven) {
371
+ // our protein-hover highlight ended — fall back to the declarative seed
372
+ // instead of wiping a column the URL/user asked to keep lit
373
+ self.setHighlightedColumns(
374
+ self.highlightColumns?.length ? self.highlightColumns : undefined,
375
+ )
376
+ proteinDriven = false
377
+ }
378
+ }
325
379
  }
326
380
 
327
381
  export function runCleanup() {
@@ -0,0 +1,50 @@
1
+ import { TabixIndexedFile } from '@gmod/tabix'
2
+ import { openLocation } from '@jbrowse/core/util/io'
3
+
4
+ // Pull one transcript's whole multiple-alignment out of a locus-keyed tabix
5
+ // file. Each line is `refName<TAB>start<TAB>end<TAB>msaId<TAB>packed`, where
6
+ // `packed` is `name:SEQ;name:SEQ;...` — no newlines, so the alignment survives
7
+ // as a single tabix column. We query the transcript's genomic locus, then pick
8
+ // the line whose msaId matches, and rebuild a FASTA string.
9
+ export async function fetchTabixMsa({
10
+ location,
11
+ indexLocation,
12
+ msaId,
13
+ refName,
14
+ start,
15
+ end,
16
+ }: {
17
+ location: { uri: string }
18
+ indexLocation?: { uri: string }
19
+ msaId: string
20
+ refName: string
21
+ start: number
22
+ end: number
23
+ }) {
24
+ const uri = (loc: { uri: string }) =>
25
+ openLocation({ uri: loc.uri, locationType: 'UriLocation' as const })
26
+ const file = new TabixIndexedFile({
27
+ filehandle: uri(location),
28
+ csiFilehandle: uri(indexLocation ?? { uri: `${location.uri}.csi` }),
29
+ })
30
+ const lines: string[] = []
31
+ await file.getLines(refName, start, end, {
32
+ lineCallback: line => {
33
+ lines.push(line)
34
+ },
35
+ })
36
+ const line = lines.find(l => l.split('\t')[3] === msaId)
37
+ return line ? unpack(line) : undefined
38
+ }
39
+
40
+ function unpack(line: string) {
41
+ const packed = line.split('\t')[4] ?? ''
42
+ return packed
43
+ .split(';')
44
+ .filter(Boolean)
45
+ .map(pair => {
46
+ const colon = pair.indexOf(':')
47
+ return `>${pair.slice(0, colon)}\n${pair.slice(colon + 1)}`
48
+ })
49
+ .join('\n')
50
+ }
@@ -471,7 +471,6 @@ export default function stateModelFactory() {
471
471
  highlightConnectedStructures,
472
472
  autoConnectStructures,
473
473
  autoLoadProteinDomains,
474
- observeProteinHighlights,
475
474
  ]) {
476
475
  addDisposer(
477
476
  self,
@@ -480,7 +479,10 @@ export default function stateModelFactory() {
480
479
  }),
481
480
  )
482
481
  }
482
+ // these two keep per-reaction state across runs (a "did I set it?" flag),
483
+ // so they're factories returning the autorun body rather than plain fns
483
484
  addDisposer(self, autorun(syncGenomeHoverToMsaColumn(self)))
485
+ addDisposer(self, autorun(observeProteinHighlights(self)))
484
486
  },
485
487
  }))
486
488
  }
@@ -1,6 +1,14 @@
1
1
  export interface MsaViewInitState {
2
2
  msaData?: string
3
3
  msaUrl?: string
4
+ // a tabix file keyed by genomic locus, where each line packs one transcript's
5
+ // whole multiple-alignment (`name:SEQ;name:SEQ;...`). The transcript's locus
6
+ // comes from the view's connectedFeature, and `msaId` (default the feature's
7
+ // name) selects its line. Lets one genome-scale alignment serve any gene
8
+ // without per-gene files. See react-msaview's gene-explorer.
9
+ msaTabixLocation?: { uri: string }
10
+ msaIndexLocation?: { uri: string }
11
+ msaId?: string
4
12
  treeData?: string
5
13
  treeUrl?: string
6
14
  querySeqName?: string
@@ -26,8 +26,7 @@ export async function getCachedDomains(accessions: string[]) {
26
26
  const tx = db.transaction(STORE_NAME, 'readonly')
27
27
  const results = await Promise.all(
28
28
  accessions.map(
29
- accession =>
30
- tx.store.get(accession) as Promise<CachedDomain | undefined>,
29
+ accession => tx.store.get(accession) as Promise<CachedDomain | undefined>,
31
30
  ),
32
31
  )
33
32
  await tx.done
@@ -83,7 +83,9 @@ function parseFeature(featureXml: string): DomainMatch | undefined {
83
83
  : noteName || quals.site_type || 'site'
84
84
  const accession = isDomain ? cddId : `${cddId}:${name}`
85
85
  return {
86
- signature: { entry: { name, description: quals.note ?? name, accession } },
86
+ signature: {
87
+ entry: { name, description: quals.note ?? name, accession },
88
+ },
87
89
  locations: [span],
88
90
  }
89
91
  }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '2.5.1'
1
+ export const version = '2.6.0'