jbrowse-plugin-msaview 2.6.3 → 2.6.4
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/AddHighlightModel/MsaToGenomeHighlight.js +8 -9
- package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +0 -2
- package/dist/MsaViewPanel/afterCreateAutoruns.js +1 -63
- package/dist/MsaViewPanel/model.d.ts +3 -28
- package/dist/MsaViewPanel/model.js +5 -103
- package/dist/MsaViewPanel/structureConnection.d.ts +0 -38
- package/dist/MsaViewPanel/structureConnection.js +0 -20
- package/dist/MsaViewPanel/structureConnection.test.js +1 -35
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +26 -26
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +8 -9
- package/src/MsaViewPanel/afterCreateAutoruns.ts +1 -84
- package/src/MsaViewPanel/model.ts +4 -135
- package/src/MsaViewPanel/structureConnection.test.ts +1 -53
- package/src/MsaViewPanel/structureConnection.ts +0 -47
- package/src/version.ts +1 -1
- package/dist/MsaViewPanel/autoConnectStructures.test.d.ts +0 -1
- package/dist/MsaViewPanel/autoConnectStructures.test.js +0 -60
- package/dist/MsaViewPanel/components/ConnectStructureDialog.d.ts +0 -7
- package/dist/MsaViewPanel/components/ConnectStructureDialog.js +0 -60
- package/dist/MsaViewPanel/pairwiseAlignment.d.ts +0 -20
- package/dist/MsaViewPanel/pairwiseAlignment.js +0 -138
- package/dist/MsaViewPanel/pairwiseAlignment.test.d.ts +0 -1
- package/dist/MsaViewPanel/pairwiseAlignment.test.js +0 -111
- package/src/MsaViewPanel/autoConnectStructures.test.ts +0 -76
- package/src/MsaViewPanel/components/ConnectStructureDialog.tsx +0 -154
- package/src/MsaViewPanel/pairwiseAlignment.test.ts +0 -140
- package/src/MsaViewPanel/pairwiseAlignment.ts +0 -182
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "2.6.
|
|
1
|
+
export declare const version = "2.6.4";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.6.
|
|
1
|
+
export const version = '2.6.4';
|
package/package.json
CHANGED
|
@@ -5,7 +5,6 @@ import { observer } from 'mobx-react'
|
|
|
5
5
|
|
|
6
6
|
import { hasHoverPosition, useStyles } from './util'
|
|
7
7
|
import { isMsaView } from '../MsaViewPanel/model'
|
|
8
|
-
import { getCanonicalRefName } from '../MsaViewPanel/util'
|
|
9
8
|
|
|
10
9
|
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
|
|
11
10
|
|
|
@@ -38,19 +37,19 @@ const MsaToGenomeHighlightRenderer = observer(function ({
|
|
|
38
37
|
highlights: { refName: string; start: number; end: number }[]
|
|
39
38
|
}) {
|
|
40
39
|
const { classes } = useStyles()
|
|
41
|
-
const { assemblyManager } = getSession(model)
|
|
42
40
|
const { offsetPx } = model
|
|
43
41
|
|
|
44
42
|
return (
|
|
45
43
|
<>
|
|
46
44
|
{highlights.map((r, idx) => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
45
|
+
// Use the highlight's own refName, which is already in the connected
|
|
46
|
+
// view's coordinate space (it comes from the connectedFeature the
|
|
47
|
+
// launcher set on this LGV). Do NOT canonicalize: bpToPx matches
|
|
48
|
+
// displayed regions by exact refName with no alias resolution, so
|
|
49
|
+
// rewriting e.g. "chr17" to the assembly-canonical "17" misses a view
|
|
50
|
+
// whose regions are "chr17". (GenomeMouseoverHighlight does the same.)
|
|
51
|
+
const s = model.bpToPx({ refName: r.refName, coord: r.start })
|
|
52
|
+
const e = model.bpToPx({ refName: r.refName, coord: r.end })
|
|
54
53
|
if (s && e) {
|
|
55
54
|
const width = Math.max(Math.abs(e.offsetPx - s.offsetPx), 4)
|
|
56
55
|
const left = Math.min(s.offsetPx, e.offsetPx) - offsetPx
|
|
@@ -10,11 +10,7 @@ import {
|
|
|
10
10
|
retrieveMsaData,
|
|
11
11
|
storeMsaData,
|
|
12
12
|
} from './msaDataStore'
|
|
13
|
-
import {
|
|
14
|
-
gappedToUngappedPosition,
|
|
15
|
-
getProteinViews,
|
|
16
|
-
structureMatchesMsa,
|
|
17
|
-
} from './structureConnection'
|
|
13
|
+
import { getProteinViews } from './structureConnection'
|
|
18
14
|
import { getUniprotIdFromAlphaFoldUrl } from './util'
|
|
19
15
|
|
|
20
16
|
import type { JBrowsePluginMsaViewModel } from './model'
|
|
@@ -199,85 +195,6 @@ export function syncGenomeHoverToMsaColumn(self: JBrowsePluginMsaViewModel) {
|
|
|
199
195
|
}
|
|
200
196
|
}
|
|
201
197
|
|
|
202
|
-
export function highlightConnectedStructures(self: JBrowsePluginMsaViewModel) {
|
|
203
|
-
const { mouseCol, connectedProteinViews } = self
|
|
204
|
-
if (connectedProteinViews.length === 0) {
|
|
205
|
-
return
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
for (const conn of connectedProteinViews) {
|
|
209
|
-
const structure = conn.proteinView.structures[conn.structureIdx]
|
|
210
|
-
if (!structure) {
|
|
211
|
-
continue
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (mouseCol === undefined) {
|
|
215
|
-
structure.clearHighlightFromExternal?.()
|
|
216
|
-
continue
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const seq = self.getSequenceByRowName(conn.msaRowName)
|
|
220
|
-
if (!seq) {
|
|
221
|
-
continue
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const msaUngapped = gappedToUngappedPosition(seq, mouseCol)
|
|
225
|
-
if (msaUngapped === undefined) {
|
|
226
|
-
structure.clearHighlightFromExternal?.()
|
|
227
|
-
continue
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const structurePos = conn.msaToStructure[msaUngapped]
|
|
231
|
-
if (structurePos === undefined) {
|
|
232
|
-
structure.clearHighlightFromExternal?.()
|
|
233
|
-
} else {
|
|
234
|
-
structure.highlightFromExternal?.(structurePos)
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
export function autoConnectStructures(self: JBrowsePluginMsaViewModel) {
|
|
240
|
-
const { connectedViewId, uniprotId, rows, connectedStructures } = self
|
|
241
|
-
|
|
242
|
-
if (rows.length === 0) {
|
|
243
|
-
return
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
for (const view of getProteinViews(getSession(self).views)) {
|
|
247
|
-
for (
|
|
248
|
-
let structureIdx = 0;
|
|
249
|
-
structureIdx < view.structures.length;
|
|
250
|
-
structureIdx++
|
|
251
|
-
) {
|
|
252
|
-
const structure = view.structures[structureIdx]
|
|
253
|
-
if (!structure) {
|
|
254
|
-
continue
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (!structureMatchesMsa({ structure, connectedViewId, uniprotId })) {
|
|
258
|
-
continue
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const alreadyConnected = connectedStructures.some(
|
|
262
|
-
c => c.proteinViewId === view.id && c.structureIdx === structureIdx,
|
|
263
|
-
)
|
|
264
|
-
if (alreadyConnected) {
|
|
265
|
-
continue
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (!structure.structureSequences?.[0]) {
|
|
269
|
-
continue
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
try {
|
|
273
|
-
self.connectToStructure(view.id, structureIdx)
|
|
274
|
-
} catch (e) {
|
|
275
|
-
console.error('Failed to auto-connect to ProteinView:', e)
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
198
|
/**
|
|
282
199
|
* Mirror a connected 3D protein view's hovered residue onto the MSA's
|
|
283
200
|
* highlighted columns. Returns the autorun body and keeps a flag tracking
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { lazy } from 'react'
|
|
2
|
-
|
|
3
1
|
import { BaseViewModel } from '@jbrowse/core/pluggableElementTypes'
|
|
4
2
|
import { getSession } from '@jbrowse/core/util'
|
|
5
3
|
import { addDisposer, types } from '@jbrowse/mobx-state-tree'
|
|
@@ -12,9 +10,7 @@ import { MSAModelF } from 'react-msaview'
|
|
|
12
10
|
export type { MSAFormat } from 'msa-parsers'
|
|
13
11
|
|
|
14
12
|
import {
|
|
15
|
-
autoConnectStructures,
|
|
16
13
|
autoLoadProteinDomains,
|
|
17
|
-
highlightConnectedStructures,
|
|
18
14
|
launchBlastIfNeeded,
|
|
19
15
|
loadStoredData,
|
|
20
16
|
observeProteinHighlights,
|
|
@@ -24,11 +20,7 @@ import {
|
|
|
24
20
|
syncGenomeHoverToMsaColumn,
|
|
25
21
|
} from './afterCreateAutoruns'
|
|
26
22
|
import { msaCoordToGenomeCoord } from './msaCoordToGenomeCoord'
|
|
27
|
-
import { buildAlignmentMaps, runPairwiseAlignment } from './pairwiseAlignment'
|
|
28
|
-
import { getProteinViews } from './structureConnection'
|
|
29
|
-
import { getCanonicalRefName } from './util'
|
|
30
23
|
|
|
31
|
-
import type { ProteinView, StructureConnection } from './structureConnection'
|
|
32
24
|
import type { MafRegion, MsaViewInitState } from './types'
|
|
33
25
|
import type {
|
|
34
26
|
BlastDatabase,
|
|
@@ -39,10 +31,6 @@ import type { Feature } from '@jbrowse/core/util'
|
|
|
39
31
|
import type { Instance } from '@jbrowse/mobx-state-tree'
|
|
40
32
|
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
|
|
41
33
|
|
|
42
|
-
const ConnectStructureDialog = lazy(
|
|
43
|
-
() => import('./components/ConnectStructureDialog'),
|
|
44
|
-
)
|
|
45
|
-
|
|
46
34
|
type LGV = LinearGenomeViewModel
|
|
47
35
|
|
|
48
36
|
type MaybeLGV = LGV | undefined
|
|
@@ -106,11 +94,6 @@ export default function stateModelFactory() {
|
|
|
106
94
|
*/
|
|
107
95
|
init: types.frozen<MsaViewInitState | undefined>(),
|
|
108
96
|
|
|
109
|
-
/**
|
|
110
|
-
* #property
|
|
111
|
-
*/
|
|
112
|
-
connectedStructures: types.array(types.frozen<StructureConnection>()),
|
|
113
|
-
|
|
114
97
|
/**
|
|
115
98
|
* #property
|
|
116
99
|
*/
|
|
@@ -194,24 +177,6 @@ export default function stateModelFactory() {
|
|
|
194
177
|
const { views } = getSession(self)
|
|
195
178
|
return views.find(f => f.id === self.connectedViewId) as MaybeLGV
|
|
196
179
|
},
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* #getter
|
|
200
|
-
*/
|
|
201
|
-
get connectedProteinViews() {
|
|
202
|
-
const proteinViews = getProteinViews(getSession(self).views)
|
|
203
|
-
const result: (StructureConnection & { proteinView: ProteinView })[] =
|
|
204
|
-
[]
|
|
205
|
-
for (const conn of self.connectedStructures) {
|
|
206
|
-
const proteinView = proteinViews.find(
|
|
207
|
-
v => v.id === conn.proteinViewId,
|
|
208
|
-
)
|
|
209
|
-
if (proteinView) {
|
|
210
|
-
result.push({ ...conn, proteinView })
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
return result
|
|
214
|
-
},
|
|
215
180
|
}))
|
|
216
181
|
|
|
217
182
|
.views(self => ({
|
|
@@ -315,94 +280,22 @@ export default function stateModelFactory() {
|
|
|
315
280
|
*/
|
|
316
281
|
handleMsaClick(coord: number) {
|
|
317
282
|
const { connectedView, zoomToBaseLevel } = self
|
|
318
|
-
const { assemblyManager } = getSession(self)
|
|
319
283
|
const r2 = msaCoordToGenomeCoord({ model: self, coord })
|
|
320
284
|
|
|
321
285
|
if (!r2 || !connectedView) {
|
|
322
286
|
return
|
|
323
287
|
}
|
|
324
288
|
|
|
289
|
+
// Use the genome coord's own refName for both nav paths — it matches the
|
|
290
|
+
// connected view's displayed regions. Canonicalizing (e.g. "chr17"->"17")
|
|
291
|
+
// can miss a view whose regions are an alias (same as the bpToPx path).
|
|
325
292
|
if (zoomToBaseLevel) {
|
|
326
293
|
connectedView.navTo(r2)
|
|
327
294
|
} else {
|
|
328
|
-
|
|
329
|
-
assemblyManager,
|
|
330
|
-
assemblyNames: connectedView.assemblyNames,
|
|
331
|
-
refName: r2.refName,
|
|
332
|
-
})
|
|
333
|
-
connectedView.centerAt(r2.start, r)
|
|
295
|
+
connectedView.centerAt(r2.start, r2.refName)
|
|
334
296
|
}
|
|
335
297
|
},
|
|
336
298
|
|
|
337
|
-
/**
|
|
338
|
-
* #action
|
|
339
|
-
*/
|
|
340
|
-
connectToStructure(
|
|
341
|
-
proteinViewId: string,
|
|
342
|
-
structureIdx: number,
|
|
343
|
-
msaRowName?: string,
|
|
344
|
-
) {
|
|
345
|
-
const rowName = msaRowName ?? self.querySeqName
|
|
346
|
-
const msaSequence = self.getSequenceByRowName(rowName)
|
|
347
|
-
if (!msaSequence) {
|
|
348
|
-
throw new Error(`MSA row "${rowName}" not found`)
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const ungappedMsaSequence = msaSequence.replaceAll('-', '')
|
|
352
|
-
|
|
353
|
-
const proteinView = getProteinViews(getSession(self).views).find(
|
|
354
|
-
v => v.id === proteinViewId,
|
|
355
|
-
)
|
|
356
|
-
if (!proteinView) {
|
|
357
|
-
throw new Error(`ProteinView "${proteinViewId}" not found`)
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const structure = proteinView.structures[structureIdx]
|
|
361
|
-
if (!structure) {
|
|
362
|
-
throw new Error(`Structure at index ${structureIdx} not found`)
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const structureSequence = structure.structureSequences?.[0]
|
|
366
|
-
if (!structureSequence) {
|
|
367
|
-
throw new Error('Structure sequence not available')
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const alignment = runPairwiseAlignment(
|
|
371
|
-
ungappedMsaSequence,
|
|
372
|
-
structureSequence,
|
|
373
|
-
)
|
|
374
|
-
const { seq1ToSeq2 } = buildAlignmentMaps(alignment)
|
|
375
|
-
|
|
376
|
-
const connection: StructureConnection = {
|
|
377
|
-
proteinViewId,
|
|
378
|
-
structureIdx,
|
|
379
|
-
msaRowName: rowName,
|
|
380
|
-
msaToStructure: Object.fromEntries(seq1ToSeq2),
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
self.connectedStructures.push(connection)
|
|
384
|
-
},
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* #action
|
|
388
|
-
*/
|
|
389
|
-
disconnectFromStructure(proteinViewId: string, structureIdx: number) {
|
|
390
|
-
const idx = self.connectedStructures.findIndex(
|
|
391
|
-
c =>
|
|
392
|
-
c.proteinViewId === proteinViewId &&
|
|
393
|
-
c.structureIdx === structureIdx,
|
|
394
|
-
)
|
|
395
|
-
if (idx !== -1) {
|
|
396
|
-
self.connectedStructures.splice(idx, 1)
|
|
397
|
-
}
|
|
398
|
-
},
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* #action
|
|
402
|
-
*/
|
|
403
|
-
disconnectAllStructures() {
|
|
404
|
-
self.connectedStructures.clear()
|
|
405
|
-
},
|
|
406
299
|
}))
|
|
407
300
|
.actions(self => {
|
|
408
301
|
const superSetMouseClickPos = self.setMouseClickPos.bind(self)
|
|
@@ -434,28 +327,6 @@ export default function stateModelFactory() {
|
|
|
434
327
|
self.setZoomToBaseLevel(!self.zoomToBaseLevel)
|
|
435
328
|
},
|
|
436
329
|
},
|
|
437
|
-
{
|
|
438
|
-
label: 'Connect to protein structure...',
|
|
439
|
-
onClick: () => {
|
|
440
|
-
getSession(self).queueDialog(handleClose => [
|
|
441
|
-
ConnectStructureDialog,
|
|
442
|
-
{
|
|
443
|
-
model: self,
|
|
444
|
-
handleClose,
|
|
445
|
-
},
|
|
446
|
-
])
|
|
447
|
-
},
|
|
448
|
-
},
|
|
449
|
-
...(self.connectedStructures.length > 0
|
|
450
|
-
? [
|
|
451
|
-
{
|
|
452
|
-
label: 'Disconnect from protein structures',
|
|
453
|
-
onClick: () => {
|
|
454
|
-
self.disconnectAllStructures()
|
|
455
|
-
},
|
|
456
|
-
},
|
|
457
|
-
]
|
|
458
|
-
: []),
|
|
459
330
|
]
|
|
460
331
|
},
|
|
461
332
|
}))
|
|
@@ -468,8 +339,6 @@ export default function stateModelFactory() {
|
|
|
468
339
|
storeDataToIndexedDB,
|
|
469
340
|
launchBlastIfNeeded,
|
|
470
341
|
processInit,
|
|
471
|
-
highlightConnectedStructures,
|
|
472
|
-
autoConnectStructures,
|
|
473
342
|
autoLoadProteinDomains,
|
|
474
343
|
]) {
|
|
475
344
|
addDisposer(
|
|
@@ -1,58 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
gappedToUngappedPosition,
|
|
5
|
-
structureMatchesMsa,
|
|
6
|
-
} from './structureConnection'
|
|
7
|
-
|
|
8
|
-
describe('structureMatchesMsa', () => {
|
|
9
|
-
test('matches on a shared genome view alone (no UniProt id)', () => {
|
|
10
|
-
expect(
|
|
11
|
-
structureMatchesMsa({
|
|
12
|
-
structure: { connectedViewId: 'lgv-TP53' },
|
|
13
|
-
connectedViewId: 'lgv-TP53',
|
|
14
|
-
}),
|
|
15
|
-
).toBe(true)
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
test('matches on a shared UniProt id alone (no genome view)', () => {
|
|
19
|
-
expect(
|
|
20
|
-
structureMatchesMsa({
|
|
21
|
-
structure: { uniprotId: 'P04637' },
|
|
22
|
-
uniprotId: 'P04637',
|
|
23
|
-
}),
|
|
24
|
-
).toBe(true)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
test('shared genome view wins even when UniProt ids differ', () => {
|
|
28
|
-
expect(
|
|
29
|
-
structureMatchesMsa({
|
|
30
|
-
structure: { connectedViewId: 'lgv-TP53', uniprotId: 'OTHER' },
|
|
31
|
-
connectedViewId: 'lgv-TP53',
|
|
32
|
-
uniprotId: 'P04637',
|
|
33
|
-
}),
|
|
34
|
-
).toBe(true)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
test('no match when neither key matches', () => {
|
|
38
|
-
expect(
|
|
39
|
-
structureMatchesMsa({
|
|
40
|
-
structure: { connectedViewId: 'lgv-OTHER', uniprotId: 'OTHER' },
|
|
41
|
-
connectedViewId: 'lgv-TP53',
|
|
42
|
-
uniprotId: 'P04637',
|
|
43
|
-
}),
|
|
44
|
-
).toBe(false)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
test('two undefined connectedViewIds do not count as a shared genome view', () => {
|
|
48
|
-
// both sides lacking a genome view must NOT auto-pair on `undefined ===
|
|
49
|
-
// undefined`; only an explicit shared id (or UniProt id) connects
|
|
50
|
-
expect(structureMatchesMsa({ structure: {} })).toBe(false)
|
|
51
|
-
expect(
|
|
52
|
-
structureMatchesMsa({ structure: { uniprotId: 'P04637' } }),
|
|
53
|
-
).toBe(false)
|
|
54
|
-
})
|
|
55
|
-
})
|
|
3
|
+
import { gappedToUngappedPosition } from './structureConnection'
|
|
56
4
|
|
|
57
5
|
describe('gappedToUngappedPosition', () => {
|
|
58
6
|
test('returns correct ungapped position for non-gap character', () => {
|
|
@@ -4,9 +4,6 @@ export interface ProteinViewStructure {
|
|
|
4
4
|
uniprotId?: string
|
|
5
5
|
structureSequences?: string[]
|
|
6
6
|
hoverGenomeHighlights?: { start: number; end: number }[]
|
|
7
|
-
hoverPosition?: { structureSeqPos?: number }
|
|
8
|
-
clearHighlightFromExternal?: () => void
|
|
9
|
-
highlightFromExternal?: (pos: number) => void
|
|
10
7
|
}
|
|
11
8
|
|
|
12
9
|
export interface ProteinView {
|
|
@@ -28,50 +25,6 @@ export function getProteinViews(views: { type: string }[]): ProteinView[] {
|
|
|
28
25
|
return (views as unknown[]).filter(isProteinView)
|
|
29
26
|
}
|
|
30
27
|
|
|
31
|
-
/**
|
|
32
|
-
* Whether a 3D structure belongs to a given alignment — the single source of
|
|
33
|
-
* truth for pairing an MsaView with a ProteinView's structure. A structure
|
|
34
|
-
* matches when it either:
|
|
35
|
-
* - shares the alignment's genome view (both pinned to the same
|
|
36
|
-
* LinearGenomeView via `connectedViewId` — the genome-centric gene-explorer
|
|
37
|
-
* flow, the same key genome↔MSA and genome↔structure already bridge through),
|
|
38
|
-
* or
|
|
39
|
-
* - shares the alignment's UniProt accession (the BLAST/AlphaFold flow, which
|
|
40
|
-
* has no genome view to bridge through).
|
|
41
|
-
*
|
|
42
|
-
* The residue map itself is built by sequence (connectToStructure pairwise-
|
|
43
|
-
* aligns the query row against the structure), so neither key is mechanically
|
|
44
|
-
* required — they only scope WHICH structure pairs with the alignment.
|
|
45
|
-
*/
|
|
46
|
-
export function structureMatchesMsa({
|
|
47
|
-
structure,
|
|
48
|
-
connectedViewId,
|
|
49
|
-
uniprotId,
|
|
50
|
-
}: {
|
|
51
|
-
structure: Pick<ProteinViewStructure, 'connectedViewId' | 'uniprotId'>
|
|
52
|
-
connectedViewId?: string
|
|
53
|
-
uniprotId?: string
|
|
54
|
-
}) {
|
|
55
|
-
const sharesGenomeView =
|
|
56
|
-
!!connectedViewId && structure.connectedViewId === connectedViewId
|
|
57
|
-
const sharesUniprot = !!uniprotId && structure.uniprotId === uniprotId
|
|
58
|
-
return sharesGenomeView || sharesUniprot
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Represents a connection between the MSA view and a protein structure
|
|
63
|
-
*/
|
|
64
|
-
export interface StructureConnection {
|
|
65
|
-
/** ID of the ProteinView containing the structure */
|
|
66
|
-
proteinViewId: string
|
|
67
|
-
/** Index of the structure within the ProteinView's structures array */
|
|
68
|
-
structureIdx: number
|
|
69
|
-
/** Name of the MSA row that corresponds to this structure */
|
|
70
|
-
msaRowName: string
|
|
71
|
-
/** Map from MSA ungapped position to structure sequence position */
|
|
72
|
-
msaToStructure: Record<number, number>
|
|
73
|
-
}
|
|
74
|
-
|
|
75
28
|
/**
|
|
76
29
|
* Helper to convert gapped MSA column to ungapped position for a specific row
|
|
77
30
|
*/
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.6.
|
|
1
|
+
export const version = '2.6.4'
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { getSession } from '@jbrowse/core/util';
|
|
2
|
-
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
3
|
-
import { autoConnectStructures } from './afterCreateAutoruns';
|
|
4
|
-
// Integration coverage for the autorun itself — the structure-matching matrix
|
|
5
|
-
// lives in structureConnection.test.ts (structureMatchesMsa). Here we check the
|
|
6
|
-
// autorun wires a match through to connectToStructure and respects its guards.
|
|
7
|
-
// Mock only getSession; keep the rest of the util module real so the
|
|
8
|
-
// afterCreateAutoruns import graph still loads.
|
|
9
|
-
vi.mock('@jbrowse/core/util', async (importOriginal) => ({
|
|
10
|
-
...(await importOriginal()),
|
|
11
|
-
getSession: vi.fn(),
|
|
12
|
-
}));
|
|
13
|
-
const mockGetSession = vi.mocked(getSession);
|
|
14
|
-
function makeModel(opts) {
|
|
15
|
-
const connected = [];
|
|
16
|
-
const model = {
|
|
17
|
-
connectedViewId: opts.connectedViewId,
|
|
18
|
-
uniprotId: opts.uniprotId,
|
|
19
|
-
rows: [['hg38', 'MKATEST']],
|
|
20
|
-
connectedStructures: connected,
|
|
21
|
-
connectToStructure: (proteinViewId, structureIdx) => {
|
|
22
|
-
connected.push({ proteinViewId, structureIdx });
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
return { model, connected };
|
|
26
|
-
}
|
|
27
|
-
function withStructure(structure) {
|
|
28
|
-
const view = {
|
|
29
|
-
type: 'ProteinView',
|
|
30
|
-
id: 'pv1',
|
|
31
|
-
structures: [{ ...structure, structureSequences: ['MKATEST'] }],
|
|
32
|
-
};
|
|
33
|
-
mockGetSession.mockReturnValue({
|
|
34
|
-
views: [view],
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
describe('autoConnectStructures', () => {
|
|
38
|
-
beforeEach(() => {
|
|
39
|
-
vi.clearAllMocks();
|
|
40
|
-
});
|
|
41
|
-
test('connects a matching structure (genome-view link, no UniProt id)', () => {
|
|
42
|
-
const { model, connected } = makeModel({ connectedViewId: 'lgv-TP53' });
|
|
43
|
-
withStructure({ connectedViewId: 'lgv-TP53' });
|
|
44
|
-
autoConnectStructures(model);
|
|
45
|
-
expect(connected).toEqual([{ proteinViewId: 'pv1', structureIdx: 0 }]);
|
|
46
|
-
});
|
|
47
|
-
test('does not connect a non-matching structure', () => {
|
|
48
|
-
const { model, connected } = makeModel({ connectedViewId: 'lgv-TP53' });
|
|
49
|
-
withStructure({ connectedViewId: 'lgv-OTHER' });
|
|
50
|
-
autoConnectStructures(model);
|
|
51
|
-
expect(connected).toEqual([]);
|
|
52
|
-
});
|
|
53
|
-
test('does not connect before the alignment has loaded (no rows)', () => {
|
|
54
|
-
const { model, connected } = makeModel({ connectedViewId: 'lgv-TP53' });
|
|
55
|
-
model.rows = [];
|
|
56
|
-
withStructure({ connectedViewId: 'lgv-TP53' });
|
|
57
|
-
autoConnectStructures(model);
|
|
58
|
-
expect(connected).toEqual([]);
|
|
59
|
-
});
|
|
60
|
-
});
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import type { JBrowsePluginMsaViewModel } from '../model';
|
|
3
|
-
declare const ConnectStructureDialog: ({ model, handleClose, }: {
|
|
4
|
-
model: JBrowsePluginMsaViewModel;
|
|
5
|
-
handleClose: () => void;
|
|
6
|
-
}) => React.JSX.Element;
|
|
7
|
-
export default ConnectStructureDialog;
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { Dialog, ErrorMessage } from '@jbrowse/core/ui';
|
|
3
|
-
import { getSession } from '@jbrowse/core/util';
|
|
4
|
-
import { Button, DialogActions, DialogContent, FormControl, InputLabel, MenuItem, Select, Typography, } from '@mui/material';
|
|
5
|
-
import { observer } from 'mobx-react';
|
|
6
|
-
import { makeStyles } from 'tss-react/mui';
|
|
7
|
-
import { getProteinViews } from '../structureConnection';
|
|
8
|
-
const useStyles = makeStyles()(theme => ({
|
|
9
|
-
formControl: {
|
|
10
|
-
marginBottom: theme.spacing(2),
|
|
11
|
-
},
|
|
12
|
-
}));
|
|
13
|
-
const ConnectStructureDialog = observer(function ConnectStructureDialog({ model, handleClose, }) {
|
|
14
|
-
const { classes } = useStyles();
|
|
15
|
-
const session = getSession(model);
|
|
16
|
-
const [selectedViewId, setSelectedViewId] = useState('');
|
|
17
|
-
const [selectedStructureIdx, setSelectedStructureIdx] = useState(0);
|
|
18
|
-
const [selectedMsaRow, setSelectedMsaRow] = useState(model.querySeqName);
|
|
19
|
-
const [error, setError] = useState();
|
|
20
|
-
const proteinViews = getProteinViews(session.views);
|
|
21
|
-
const selectedView = proteinViews.find(v => v.id === selectedViewId);
|
|
22
|
-
const structures = selectedView?.structures ?? [];
|
|
23
|
-
const msaRowNames = model.rows.map(r => r[0]);
|
|
24
|
-
const handleConnect = () => {
|
|
25
|
-
if (!selectedViewId) {
|
|
26
|
-
setError('Please select a protein view');
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
try {
|
|
30
|
-
model.connectToStructure(selectedViewId, selectedStructureIdx, selectedMsaRow);
|
|
31
|
-
handleClose();
|
|
32
|
-
}
|
|
33
|
-
catch (e) {
|
|
34
|
-
setError(e instanceof Error ? e.message : String(e));
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
return (React.createElement(Dialog, { maxWidth: "sm", title: "Connect to Protein Structure", open: true, onClose: handleClose },
|
|
38
|
-
React.createElement(DialogContent, null, proteinViews.length === 0 ? (React.createElement(Typography, { color: "textSecondary" }, "No protein views are currently open. Please open a protein structure view first.")) : (React.createElement(React.Fragment, null,
|
|
39
|
-
React.createElement(FormControl, { fullWidth: true, className: classes.formControl },
|
|
40
|
-
React.createElement(InputLabel, null, "Protein View"),
|
|
41
|
-
React.createElement(Select, { value: selectedViewId, label: "Protein View", onChange: e => {
|
|
42
|
-
setSelectedViewId(e.target.value);
|
|
43
|
-
setSelectedStructureIdx(0);
|
|
44
|
-
} }, proteinViews.map(view => (React.createElement(MenuItem, { key: view.id, value: view.id }, view.displayName ?? `ProteinView ${view.id}`))))),
|
|
45
|
-
structures.length > 1 ? (React.createElement(FormControl, { fullWidth: true, className: classes.formControl },
|
|
46
|
-
React.createElement(InputLabel, null, "Structure"),
|
|
47
|
-
React.createElement(Select, { value: selectedStructureIdx, label: "Structure", onChange: e => {
|
|
48
|
-
setSelectedStructureIdx(e.target.value);
|
|
49
|
-
} }, structures.map((structure, idx) => (React.createElement(MenuItem, { key: idx, value: idx }, structure.url ?? `Structure ${idx + 1}`)))))) : null,
|
|
50
|
-
React.createElement(FormControl, { fullWidth: true, className: classes.formControl },
|
|
51
|
-
React.createElement(InputLabel, null, "MSA Row"),
|
|
52
|
-
React.createElement(Select, { value: selectedMsaRow, label: "MSA Row", onChange: e => {
|
|
53
|
-
setSelectedMsaRow(e.target.value);
|
|
54
|
-
} }, msaRowNames.map(name => (React.createElement(MenuItem, { key: name, value: name }, name))))),
|
|
55
|
-
error ? React.createElement(ErrorMessage, { error: error }) : null))),
|
|
56
|
-
React.createElement(DialogActions, null,
|
|
57
|
-
React.createElement(Button, { onClick: handleClose }, "Cancel"),
|
|
58
|
-
React.createElement(Button, { onClick: handleConnect, variant: "contained", disabled: proteinViews.length === 0 || !selectedViewId }, "Connect"))));
|
|
59
|
-
});
|
|
60
|
-
export default ConnectStructureDialog;
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
interface AlignmentResult {
|
|
2
|
-
alignedSeq1: string;
|
|
3
|
-
alignedSeq2: string;
|
|
4
|
-
score: number;
|
|
5
|
-
}
|
|
6
|
-
export interface AlignmentRow {
|
|
7
|
-
id: string;
|
|
8
|
-
seq: string;
|
|
9
|
-
}
|
|
10
|
-
export interface PairwiseAlignment {
|
|
11
|
-
consensus: string;
|
|
12
|
-
alns: readonly [AlignmentRow, AlignmentRow];
|
|
13
|
-
}
|
|
14
|
-
export declare function needlemanWunsch(seq1: string, seq2: string, gapOpen?: number, gapExtend?: number): AlignmentResult;
|
|
15
|
-
export declare function runPairwiseAlignment(seq1: string, seq2: string): PairwiseAlignment;
|
|
16
|
-
export declare function buildAlignmentMaps(pairwiseAlignment: PairwiseAlignment): {
|
|
17
|
-
seq1ToSeq2: Map<number, number>;
|
|
18
|
-
seq2ToSeq1: Map<number, number>;
|
|
19
|
-
};
|
|
20
|
-
export {};
|