jbrowse-plugin-msaview 2.6.1 → 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 +16 -18
- package/dist/MsaViewPanel/afterCreateAutoruns.js +11 -28
- 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 +26 -26
- 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 +49 -57
- package/src/MsaViewPanel/afterCreateAutoruns.ts +11 -36
- 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 -6
- package/src/version.ts +1 -1
|
@@ -1,33 +1,31 @@
|
|
|
1
1
|
export default function LaunchMsaViewExtensionPointF(pluginManager) {
|
|
2
|
-
pluginManager.addToExtensionPoint('LaunchView-MsaView',
|
|
3
|
-
|
|
4
|
-
({ session, data, msaFileLocation, msaIndexedLocation, msaName, treeFileLocation, connectedViewId, connectedFeature, displayName, colorSchemeName, colWidth, rowHeight, treeAreaWidth, treeWidth, drawNodeBubbles, labelsAlignRight, showBranchLen, querySeqName, highlightColumns, }) => {
|
|
2
|
+
pluginManager.addToExtensionPoint('LaunchView-MsaView', (args) => {
|
|
3
|
+
const { session, data, msaFileLocation, msaIndexedLocation, msaName, treeFileLocation, querySeqName, ...rest } = args;
|
|
5
4
|
if (!data && !msaFileLocation && !msaIndexedLocation) {
|
|
6
5
|
throw new Error('No MSA data or file location provided when launching MSA view');
|
|
7
6
|
}
|
|
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).
|
|
8
10
|
session.addView('MsaView', {
|
|
9
11
|
type: 'MsaView',
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
showBranchLen,
|
|
21
|
-
highlightColumns,
|
|
12
|
+
...rest,
|
|
13
|
+
data,
|
|
14
|
+
...(treeFileLocation
|
|
15
|
+
? {
|
|
16
|
+
treeFilehandle: {
|
|
17
|
+
...treeFileLocation,
|
|
18
|
+
locationType: 'UriLocation',
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
: {}),
|
|
22
22
|
init: {
|
|
23
|
-
msaData: data?.msa,
|
|
24
|
-
treeData: data?.tree,
|
|
25
23
|
msaUrl: msaFileLocation?.uri,
|
|
26
24
|
msaIndexedLocation,
|
|
27
25
|
msaName,
|
|
28
|
-
treeUrl: treeFileLocation?.uri,
|
|
29
26
|
querySeqName,
|
|
30
27
|
},
|
|
31
28
|
});
|
|
29
|
+
return args;
|
|
32
30
|
});
|
|
33
31
|
}
|
|
@@ -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,13 +116,18 @@ export function autoLoadProteinDomains(self) {
|
|
|
116
116
|
})();
|
|
117
117
|
}
|
|
118
118
|
}
|
|
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.
|
|
119
124
|
export function processInit(self) {
|
|
120
125
|
const { init } = self;
|
|
121
126
|
if (init) {
|
|
127
|
+
const { msaUrl, msaIndexedLocation, msaName, querySeqName } = init;
|
|
122
128
|
void (async () => {
|
|
123
129
|
try {
|
|
124
130
|
self.setError(undefined);
|
|
125
|
-
const { msaData, msaUrl, msaIndexedLocation, msaName, treeData, treeUrl, querySeqName, } = init;
|
|
126
131
|
if (msaUrl) {
|
|
127
132
|
const id = getUniprotIdFromAlphaFoldUrl(msaUrl);
|
|
128
133
|
if (id) {
|
|
@@ -133,16 +138,8 @@ export function processInit(self) {
|
|
|
133
138
|
if (querySeqName) {
|
|
134
139
|
self.setQuerySeqName(querySeqName);
|
|
135
140
|
}
|
|
136
|
-
if (
|
|
137
|
-
self.
|
|
138
|
-
}
|
|
139
|
-
else if (msaUrl) {
|
|
140
|
-
const response = await fetch(msaUrl);
|
|
141
|
-
if (!response.ok) {
|
|
142
|
-
throw new Error(`Failed to fetch MSA: ${response.status}`);
|
|
143
|
-
}
|
|
144
|
-
const data = await response.text();
|
|
145
|
-
self.setMSA(data);
|
|
141
|
+
if (msaUrl) {
|
|
142
|
+
self.setMSAFilehandle({ uri: msaUrl, locationType: 'UriLocation' });
|
|
146
143
|
}
|
|
147
144
|
else if (msaIndexedLocation && msaName) {
|
|
148
145
|
const fasta = await fetchIndexedMsa({
|
|
@@ -156,17 +153,6 @@ export function processInit(self) {
|
|
|
156
153
|
throw new Error(`No alignment named ${msaName} in ${msaIndexedLocation.uri}`);
|
|
157
154
|
}
|
|
158
155
|
}
|
|
159
|
-
if (treeData) {
|
|
160
|
-
self.setTree(treeData);
|
|
161
|
-
}
|
|
162
|
-
else if (treeUrl) {
|
|
163
|
-
const response = await fetch(treeUrl);
|
|
164
|
-
if (!response.ok) {
|
|
165
|
-
throw new Error(`Failed to fetch tree: ${response.status}`);
|
|
166
|
-
}
|
|
167
|
-
const data = await response.text();
|
|
168
|
-
self.setTree(data);
|
|
169
|
-
}
|
|
170
156
|
self.setInit(undefined);
|
|
171
157
|
}
|
|
172
158
|
catch (e) {
|
|
@@ -231,7 +217,7 @@ export function highlightConnectedStructures(self) {
|
|
|
231
217
|
}
|
|
232
218
|
export function autoConnectStructures(self) {
|
|
233
219
|
const { connectedViewId, uniprotId, rows, connectedStructures } = self;
|
|
234
|
-
if (
|
|
220
|
+
if (rows.length === 0) {
|
|
235
221
|
return;
|
|
236
222
|
}
|
|
237
223
|
for (const view of getProteinViews(getSession(self).views)) {
|
|
@@ -240,10 +226,7 @@ export function autoConnectStructures(self) {
|
|
|
240
226
|
if (!structure) {
|
|
241
227
|
continue;
|
|
242
228
|
}
|
|
243
|
-
if (structure
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
if (structure.uniprotId !== uniprotId) {
|
|
229
|
+
if (!structureMatchesMsa({ structure, connectedViewId, uniprotId })) {
|
|
247
230
|
continue;
|
|
248
231
|
}
|
|
249
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;
|