jbrowse-plugin-msaview 2.6.2 → 2.6.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.
- package/dist/LaunchMsaViewExtensionPoint/index.js +12 -5
- package/dist/MsaViewPanel/afterCreateAutoruns.js +10 -21
- package/dist/MsaViewPanel/autoConnectStructures.test.d.ts +1 -0
- package/dist/MsaViewPanel/autoConnectStructures.test.js +60 -0
- package/dist/MsaViewPanel/structureConnection.d.ts +20 -0
- package/dist/MsaViewPanel/structureConnection.js +20 -0
- package/dist/MsaViewPanel/structureConnection.test.js +35 -1
- package/dist/MsaViewPanel/types.d.ts +1 -4
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +20 -20
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/LaunchMsaViewExtensionPoint/index.ts +12 -5
- package/src/MsaViewPanel/afterCreateAutoruns.ts +10 -28
- package/src/MsaViewPanel/autoConnectStructures.test.ts +76 -0
- package/src/MsaViewPanel/structureConnection.test.ts +53 -1
- package/src/MsaViewPanel/structureConnection.ts +30 -0
- package/src/MsaViewPanel/types.ts +14 -9
- package/src/version.ts +1 -1
|
@@ -4,18 +4,25 @@ export default function LaunchMsaViewExtensionPointF(pluginManager) {
|
|
|
4
4
|
if (!data && !msaFileLocation && !msaIndexedLocation) {
|
|
5
5
|
throw new Error('No MSA data or file location provided when launching MSA view');
|
|
6
6
|
}
|
|
7
|
-
//
|
|
8
|
-
//
|
|
7
|
+
// inline data and the tree URL are native react-msaview snapshot props, set
|
|
8
|
+
// directly. Only sources needing launch-time resolution go through `init`:
|
|
9
|
+
// msaUrl (AlphaFold sniff) and the name-indexed bgzip block (no native loader).
|
|
9
10
|
session.addView('MsaView', {
|
|
10
11
|
type: 'MsaView',
|
|
11
12
|
...rest,
|
|
13
|
+
data,
|
|
14
|
+
...(treeFileLocation
|
|
15
|
+
? {
|
|
16
|
+
treeFilehandle: {
|
|
17
|
+
...treeFileLocation,
|
|
18
|
+
locationType: 'UriLocation',
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
: {}),
|
|
12
22
|
init: {
|
|
13
|
-
msaData: data?.msa,
|
|
14
|
-
treeData: data?.tree,
|
|
15
23
|
msaUrl: msaFileLocation?.uri,
|
|
16
24
|
msaIndexedLocation,
|
|
17
25
|
msaName,
|
|
18
|
-
treeUrl: treeFileLocation?.uri,
|
|
19
26
|
querySeqName,
|
|
20
27
|
},
|
|
21
28
|
});
|
|
@@ -4,7 +4,7 @@ import { fetchIndexedMsa } from './fetchIndexedMsa';
|
|
|
4
4
|
import { genomeToMSA } from './genomeToMSA';
|
|
5
5
|
import { loadProteinDomains } from './loadProteinDomains';
|
|
6
6
|
import { cleanupOldData, generateDataStoreId, retrieveMsaData, storeMsaData, } from './msaDataStore';
|
|
7
|
-
import { gappedToUngappedPosition, getProteinViews, } from './structureConnection';
|
|
7
|
+
import { gappedToUngappedPosition, getProteinViews, structureMatchesMsa, } from './structureConnection';
|
|
8
8
|
import { getUniprotIdFromAlphaFoldUrl } from './util';
|
|
9
9
|
export function loadStoredData(self) {
|
|
10
10
|
const { dataStoreId, rows } = self;
|
|
@@ -116,14 +116,15 @@ export function autoLoadProteinDomains(self) {
|
|
|
116
116
|
})();
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
|
-
// Resolve the declarative `init` launch contract once
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
119
|
+
// Resolve the declarative `init` launch contract once, then clear it. msaUrl is
|
|
120
|
+
// handed to react-msaview's native filehandle loader (openLocation + progress +
|
|
121
|
+
// abort + CORS-proxy) and sniffed for an AlphaFold uniprotId; the bgzip
|
|
122
|
+
// name-indexed block is the one source with no native loader, so it's fetched
|
|
123
|
+
// here. Inline data and tree URLs arrive as native snapshot props, not via init.
|
|
123
124
|
export function processInit(self) {
|
|
124
125
|
const { init } = self;
|
|
125
126
|
if (init) {
|
|
126
|
-
const {
|
|
127
|
+
const { msaUrl, msaIndexedLocation, msaName, querySeqName } = init;
|
|
127
128
|
void (async () => {
|
|
128
129
|
try {
|
|
129
130
|
self.setError(undefined);
|
|
@@ -137,10 +138,7 @@ export function processInit(self) {
|
|
|
137
138
|
if (querySeqName) {
|
|
138
139
|
self.setQuerySeqName(querySeqName);
|
|
139
140
|
}
|
|
140
|
-
if (
|
|
141
|
-
self.setMSA(msaData);
|
|
142
|
-
}
|
|
143
|
-
else if (msaUrl) {
|
|
141
|
+
if (msaUrl) {
|
|
144
142
|
self.setMSAFilehandle({ uri: msaUrl, locationType: 'UriLocation' });
|
|
145
143
|
}
|
|
146
144
|
else if (msaIndexedLocation && msaName) {
|
|
@@ -155,12 +153,6 @@ export function processInit(self) {
|
|
|
155
153
|
throw new Error(`No alignment named ${msaName} in ${msaIndexedLocation.uri}`);
|
|
156
154
|
}
|
|
157
155
|
}
|
|
158
|
-
if (treeData) {
|
|
159
|
-
self.setTree(treeData);
|
|
160
|
-
}
|
|
161
|
-
else if (treeUrl) {
|
|
162
|
-
self.setTreeFilehandle({ uri: treeUrl, locationType: 'UriLocation' });
|
|
163
|
-
}
|
|
164
156
|
self.setInit(undefined);
|
|
165
157
|
}
|
|
166
158
|
catch (e) {
|
|
@@ -225,7 +217,7 @@ export function highlightConnectedStructures(self) {
|
|
|
225
217
|
}
|
|
226
218
|
export function autoConnectStructures(self) {
|
|
227
219
|
const { connectedViewId, uniprotId, rows, connectedStructures } = self;
|
|
228
|
-
if (
|
|
220
|
+
if (rows.length === 0) {
|
|
229
221
|
return;
|
|
230
222
|
}
|
|
231
223
|
for (const view of getProteinViews(getSession(self).views)) {
|
|
@@ -234,10 +226,7 @@ export function autoConnectStructures(self) {
|
|
|
234
226
|
if (!structure) {
|
|
235
227
|
continue;
|
|
236
228
|
}
|
|
237
|
-
if (structure
|
|
238
|
-
continue;
|
|
239
|
-
}
|
|
240
|
-
if (structure.uniprotId !== uniprotId) {
|
|
229
|
+
if (!structureMatchesMsa({ structure, connectedViewId, uniprotId })) {
|
|
241
230
|
continue;
|
|
242
231
|
}
|
|
243
232
|
const alreadyConnected = connectedStructures.some(c => c.proteinViewId === view.id && c.structureIdx === structureIdx);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
});
|
|
@@ -26,6 +26,26 @@ export declare function isProteinView(view: unknown): view is ProteinView;
|
|
|
26
26
|
export declare function getProteinViews(views: {
|
|
27
27
|
type: string;
|
|
28
28
|
}[]): ProteinView[];
|
|
29
|
+
/**
|
|
30
|
+
* Whether a 3D structure belongs to a given alignment — the single source of
|
|
31
|
+
* truth for pairing an MsaView with a ProteinView's structure. A structure
|
|
32
|
+
* matches when it either:
|
|
33
|
+
* - shares the alignment's genome view (both pinned to the same
|
|
34
|
+
* LinearGenomeView via `connectedViewId` — the genome-centric gene-explorer
|
|
35
|
+
* flow, the same key genome↔MSA and genome↔structure already bridge through),
|
|
36
|
+
* or
|
|
37
|
+
* - shares the alignment's UniProt accession (the BLAST/AlphaFold flow, which
|
|
38
|
+
* has no genome view to bridge through).
|
|
39
|
+
*
|
|
40
|
+
* The residue map itself is built by sequence (connectToStructure pairwise-
|
|
41
|
+
* aligns the query row against the structure), so neither key is mechanically
|
|
42
|
+
* required — they only scope WHICH structure pairs with the alignment.
|
|
43
|
+
*/
|
|
44
|
+
export declare function structureMatchesMsa({ structure, connectedViewId, uniprotId, }: {
|
|
45
|
+
structure: Pick<ProteinViewStructure, 'connectedViewId' | 'uniprotId'>;
|
|
46
|
+
connectedViewId?: string;
|
|
47
|
+
uniprotId?: string;
|
|
48
|
+
}): boolean;
|
|
29
49
|
/**
|
|
30
50
|
* Represents a connection between the MSA view and a protein structure
|
|
31
51
|
*/
|
|
@@ -8,6 +8,26 @@ export function isProteinView(view) {
|
|
|
8
8
|
export function getProteinViews(views) {
|
|
9
9
|
return views.filter(isProteinView);
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Whether a 3D structure belongs to a given alignment — the single source of
|
|
13
|
+
* truth for pairing an MsaView with a ProteinView's structure. A structure
|
|
14
|
+
* matches when it either:
|
|
15
|
+
* - shares the alignment's genome view (both pinned to the same
|
|
16
|
+
* LinearGenomeView via `connectedViewId` — the genome-centric gene-explorer
|
|
17
|
+
* flow, the same key genome↔MSA and genome↔structure already bridge through),
|
|
18
|
+
* or
|
|
19
|
+
* - shares the alignment's UniProt accession (the BLAST/AlphaFold flow, which
|
|
20
|
+
* has no genome view to bridge through).
|
|
21
|
+
*
|
|
22
|
+
* The residue map itself is built by sequence (connectToStructure pairwise-
|
|
23
|
+
* aligns the query row against the structure), so neither key is mechanically
|
|
24
|
+
* required — they only scope WHICH structure pairs with the alignment.
|
|
25
|
+
*/
|
|
26
|
+
export function structureMatchesMsa({ structure, connectedViewId, uniprotId, }) {
|
|
27
|
+
const sharesGenomeView = !!connectedViewId && structure.connectedViewId === connectedViewId;
|
|
28
|
+
const sharesUniprot = !!uniprotId && structure.uniprotId === uniprotId;
|
|
29
|
+
return sharesGenomeView || sharesUniprot;
|
|
30
|
+
}
|
|
11
31
|
/**
|
|
12
32
|
* Helper to convert gapped MSA column to ungapped position for a specific row
|
|
13
33
|
*/
|
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest';
|
|
2
|
-
import { gappedToUngappedPosition } from './structureConnection';
|
|
2
|
+
import { gappedToUngappedPosition, structureMatchesMsa, } from './structureConnection';
|
|
3
|
+
describe('structureMatchesMsa', () => {
|
|
4
|
+
test('matches on a shared genome view alone (no UniProt id)', () => {
|
|
5
|
+
expect(structureMatchesMsa({
|
|
6
|
+
structure: { connectedViewId: 'lgv-TP53' },
|
|
7
|
+
connectedViewId: 'lgv-TP53',
|
|
8
|
+
})).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
test('matches on a shared UniProt id alone (no genome view)', () => {
|
|
11
|
+
expect(structureMatchesMsa({
|
|
12
|
+
structure: { uniprotId: 'P04637' },
|
|
13
|
+
uniprotId: 'P04637',
|
|
14
|
+
})).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
test('shared genome view wins even when UniProt ids differ', () => {
|
|
17
|
+
expect(structureMatchesMsa({
|
|
18
|
+
structure: { connectedViewId: 'lgv-TP53', uniprotId: 'OTHER' },
|
|
19
|
+
connectedViewId: 'lgv-TP53',
|
|
20
|
+
uniprotId: 'P04637',
|
|
21
|
+
})).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
test('no match when neither key matches', () => {
|
|
24
|
+
expect(structureMatchesMsa({
|
|
25
|
+
structure: { connectedViewId: 'lgv-OTHER', uniprotId: 'OTHER' },
|
|
26
|
+
connectedViewId: 'lgv-TP53',
|
|
27
|
+
uniprotId: 'P04637',
|
|
28
|
+
})).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
test('two undefined connectedViewIds do not count as a shared genome view', () => {
|
|
31
|
+
// both sides lacking a genome view must NOT auto-pair on `undefined ===
|
|
32
|
+
// undefined`; only an explicit shared id (or UniProt id) connects
|
|
33
|
+
expect(structureMatchesMsa({ structure: {} })).toBe(false);
|
|
34
|
+
expect(structureMatchesMsa({ structure: { uniprotId: 'P04637' } })).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
3
37
|
describe('gappedToUngappedPosition', () => {
|
|
4
38
|
test('returns correct ungapped position for non-gap character', () => {
|
|
5
39
|
const seq = 'M-KA-A';
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
export interface MsaViewInitState {
|
|
2
|
-
msaData?: string;
|
|
3
2
|
msaUrl?: string;
|
|
3
|
+
querySeqName?: string;
|
|
4
4
|
msaIndexedLocation?: {
|
|
5
5
|
uri: string;
|
|
6
6
|
};
|
|
7
7
|
msaName?: string;
|
|
8
|
-
treeData?: string;
|
|
9
|
-
treeUrl?: string;
|
|
10
|
-
querySeqName?: string;
|
|
11
8
|
}
|
|
12
9
|
export interface MafRegion {
|
|
13
10
|
refName: string;
|