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.
- package/dist/LaunchMsaViewExtensionPoint/index.js +5 -2
- package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +15 -1
- package/dist/MsaViewPanel/afterCreateAutoruns.js +73 -27
- package/dist/MsaViewPanel/fetchTabixMsa.d.ts +12 -0
- package/dist/MsaViewPanel/fetchTabixMsa.js +33 -0
- package/dist/MsaViewPanel/model.js +3 -1
- package/dist/MsaViewPanel/types.d.ts +7 -0
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +29 -27
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/ncbiDomains.js +3 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +17 -16
- package/src/LaunchMsaViewExtensionPoint/index.ts +10 -1
- package/src/MsaViewPanel/afterCreateAutoruns.ts +84 -30
- package/src/MsaViewPanel/fetchTabixMsa.ts +50 -0
- package/src/MsaViewPanel/model.ts +3 -1
- package/src/MsaViewPanel/types.ts +8 -0
- package/src/utils/domainCache.ts +1 -2
- package/src/utils/ncbiDomains.ts +3 -1
- package/src/version.ts +1 -1
|
@@ -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: {
|
|
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.
|
|
1
|
+
export declare const version = "2.6.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.
|
|
1
|
+
export const version = '2.6.0';
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.
|
|
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.
|
|
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.
|
|
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.
|
|
37
|
-
"@types/node": "^25.9.
|
|
38
|
-
"@types/react": "^19.2.
|
|
39
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
40
|
-
"@typescript-eslint/parser": "^8.
|
|
41
|
-
"esbuild": "^0.28.
|
|
42
|
-
"eslint": "^10.
|
|
43
|
-
"eslint-plugin-import-x": "^4.
|
|
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.
|
|
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.
|
|
53
|
+
"prettier": "^3.8.4",
|
|
53
54
|
"pretty-bytes": "^7.1.0",
|
|
54
|
-
"puppeteer": "^25.1
|
|
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.
|
|
63
|
-
"vitest": "^4.1.
|
|
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 {
|
|
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
|
-
|
|
329
|
+
let proteinDriven = false
|
|
330
|
+
return () => {
|
|
331
|
+
const { connectedViewId, transcriptToMsaMap, querySeqName } = self
|
|
287
332
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
333
|
+
if (!connectedViewId || !transcriptToMsaMap) {
|
|
334
|
+
return
|
|
335
|
+
}
|
|
291
336
|
|
|
292
|
-
|
|
337
|
+
const columns = new Set<number>()
|
|
293
338
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
345
|
+
const highlights = structure.hoverGenomeHighlights
|
|
346
|
+
if (!highlights || highlights.length === 0) {
|
|
347
|
+
continue
|
|
348
|
+
}
|
|
304
349
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
323
|
-
|
|
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
|
package/src/utils/domainCache.ts
CHANGED
|
@@ -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
|
package/src/utils/ncbiDomains.ts
CHANGED
|
@@ -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: {
|
|
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.
|
|
1
|
+
export const version = '2.6.0'
|