jbrowse-plugin-msaview 3.4.0 → 3.5.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/README.md +7 -0
- package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +23 -17
- package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -0
- package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +5 -3
- package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +24 -4
- package/dist/LaunchMsaView/components/BlastQuery/consts.js +44 -2
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.d.ts +7 -0
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.js +42 -0
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.js +32 -0
- package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +1 -0
- package/dist/LaunchMsaView/components/ManualMSALoader/launchView.d.ts +3 -1
- package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js +2 -1
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +10 -4
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +4 -1
- package/dist/LaunchMsaView/components/QueryRowSelector.js +21 -5
- package/dist/LaunchMsaView/detectQueryRow.d.ts +11 -0
- package/dist/LaunchMsaView/detectQueryRow.js +11 -4
- package/dist/LaunchMsaView/detectQueryRow.test.js +30 -0
- package/dist/LaunchMsaView/useQueryRowName.d.ts +1 -0
- package/dist/LaunchMsaView/useQueryRowName.js +5 -1
- package/dist/LaunchMsaView/useQueryRowName.test.d.ts +1 -0
- package/dist/LaunchMsaView/useQueryRowName.test.js +23 -0
- package/dist/LaunchMsaViewExtensionPoint/index.js +19 -12
- package/dist/LaunchMsaViewExtensionPoint/index.test.d.ts +1 -0
- package/dist/LaunchMsaViewExtensionPoint/index.test.js +64 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +16 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.js +138 -86
- package/dist/MsaViewPanel/components/LaunchProgress.js +23 -5
- package/dist/MsaViewPanel/components/MsaViewPanel.js +6 -2
- package/dist/MsaViewPanel/components/MsaViewPanel.test.js +40 -0
- package/dist/MsaViewPanel/doLaunchBlast.d.ts +11 -1
- package/dist/MsaViewPanel/doLaunchBlast.js +79 -96
- package/dist/MsaViewPanel/doLaunchBlast.test.d.ts +1 -0
- package/dist/MsaViewPanel/doLaunchBlast.test.js +158 -0
- package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +3 -1
- package/dist/MsaViewPanel/doLaunchOrthologs.js +46 -10
- package/dist/MsaViewPanel/doLaunchOrthologs.test.js +48 -24
- package/dist/MsaViewPanel/genomeToMSA.js +4 -4
- package/dist/MsaViewPanel/genomeToMSA.test.js +67 -0
- package/dist/MsaViewPanel/loadProteinDomains.d.ts +8 -2
- package/dist/MsaViewPanel/loadProteinDomains.js +18 -9
- package/dist/MsaViewPanel/loadProteinDomains.test.d.ts +1 -0
- package/dist/MsaViewPanel/loadProteinDomains.test.js +41 -0
- package/dist/MsaViewPanel/model.d.ts +306 -85
- package/dist/MsaViewPanel/model.js +169 -42
- package/dist/MsaViewPanel/model.test.d.ts +1 -0
- package/dist/MsaViewPanel/model.test.js +100 -0
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.d.ts +12 -1
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.js +8 -8
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.js +133 -32
- package/dist/MsaViewPanel/msaDataStore.d.ts +7 -3
- package/dist/MsaViewPanel/msaDataStore.js +26 -7
- package/dist/MsaViewPanel/msaDataStore.test.d.ts +1 -0
- package/dist/MsaViewPanel/msaDataStore.test.js +44 -0
- package/dist/MsaViewPanel/observeProteinHighlights.test.js +20 -3
- package/dist/MsaViewPanel/processInit.test.d.ts +1 -0
- package/dist/MsaViewPanel/processInit.test.js +72 -0
- package/dist/MsaViewPanel/resolveConnectedTranscript.d.ts +9 -0
- package/dist/MsaViewPanel/resolveConnectedTranscript.js +122 -0
- package/dist/MsaViewPanel/runLaunch.d.ts +39 -0
- package/dist/MsaViewPanel/runLaunch.js +100 -0
- package/dist/MsaViewPanel/runLaunch.test.d.ts +1 -0
- package/dist/MsaViewPanel/runLaunch.test.js +175 -0
- package/dist/MsaViewPanel/storedData.test.d.ts +1 -0
- package/dist/MsaViewPanel/storedData.test.js +160 -0
- package/dist/MsaViewPanel/structureConnection.d.ts +0 -4
- package/dist/MsaViewPanel/structureConnection.js +0 -19
- package/dist/MsaViewPanel/util.d.ts +39 -0
- package/dist/MsaViewPanel/util.js +44 -0
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +46 -45
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/blastCache.js +32 -0
- package/dist/utils/blastCache.test.d.ts +1 -0
- package/dist/utils/blastCache.test.js +72 -0
- package/dist/utils/browserAlign.d.ts +42 -0
- package/dist/utils/browserAlign.js +297 -0
- package/dist/utils/browserAlign.test.d.ts +1 -0
- package/dist/utils/browserAlign.test.js +85 -0
- package/dist/utils/ebiBlast.d.ts +13 -2
- package/dist/utils/ebiBlast.js +32 -4
- package/dist/utils/ebiJobDispatcher.d.ts +6 -3
- package/dist/utils/ebiJobDispatcher.js +13 -6
- package/dist/utils/fetch.d.ts +8 -1
- package/dist/utils/fetch.js +28 -2
- package/dist/utils/homologSearch.d.ts +31 -0
- package/dist/utils/homologSearch.js +6 -0
- package/dist/utils/msa.d.ts +5 -11
- package/dist/utils/msa.js +19 -30
- package/dist/utils/msaRows.d.ts +11 -8
- package/dist/utils/msaRows.js +14 -12
- package/dist/utils/ncbiDomains.d.ts +2 -2
- package/dist/utils/ncbiOrthologs.d.ts +11 -1
- package/dist/utils/ncbiOrthologs.js +26 -7
- package/dist/utils/ncbiOrthologs.test.js +23 -1
- package/dist/utils/pantherOrthologs.js +2 -10
- package/dist/utils/phmmer.d.ts +12 -1
- package/dist/utils/phmmer.js +52 -13
- package/dist/utils/poll.d.ts +6 -1
- package/dist/utils/poll.js +7 -2
- package/dist/utils/unirefHomologs.d.ts +94 -0
- package/dist/utils/unirefHomologs.js +193 -0
- package/dist/utils/unirefHomologs.test.d.ts +1 -0
- package/dist/utils/unirefHomologs.test.js +118 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +17 -16
- package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +40 -19
- package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -0
- package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +7 -2
- package/src/LaunchMsaView/components/BlastQuery/consts.ts +50 -2
- package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.ts +43 -0
- package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.ts +64 -0
- package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -0
- package/src/LaunchMsaView/components/ManualMSALoader/launchView.ts +4 -0
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +12 -5
- package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +4 -1
- package/src/LaunchMsaView/components/QueryRowSelector.tsx +35 -7
- package/src/LaunchMsaView/detectQueryRow.test.ts +34 -0
- package/src/LaunchMsaView/detectQueryRow.ts +22 -4
- package/src/LaunchMsaView/useQueryRowName.test.ts +27 -0
- package/src/LaunchMsaView/useQueryRowName.ts +5 -1
- package/src/LaunchMsaViewExtensionPoint/index.test.ts +74 -0
- package/src/LaunchMsaViewExtensionPoint/index.ts +53 -12
- package/src/MsaViewPanel/afterCreateAutoruns.ts +150 -81
- package/src/MsaViewPanel/components/LaunchProgress.tsx +46 -4
- package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +43 -0
- package/src/MsaViewPanel/components/MsaViewPanel.tsx +8 -2
- package/src/MsaViewPanel/doLaunchBlast.test.ts +207 -0
- package/src/MsaViewPanel/doLaunchBlast.ts +94 -140
- package/src/MsaViewPanel/doLaunchOrthologs.test.ts +54 -26
- package/src/MsaViewPanel/doLaunchOrthologs.ts +63 -9
- package/src/MsaViewPanel/genomeToMSA.test.ts +76 -0
- package/src/MsaViewPanel/genomeToMSA.ts +8 -4
- package/src/MsaViewPanel/loadProteinDomains.test.ts +52 -0
- package/src/MsaViewPanel/loadProteinDomains.ts +24 -11
- package/src/MsaViewPanel/model.test.ts +121 -0
- package/src/MsaViewPanel/model.ts +200 -49
- package/src/MsaViewPanel/msaCoordToGenomeCoord.test.ts +152 -32
- package/src/MsaViewPanel/msaCoordToGenomeCoord.ts +21 -11
- package/src/MsaViewPanel/msaDataStore.test.ts +54 -0
- package/src/MsaViewPanel/msaDataStore.ts +32 -13
- package/src/MsaViewPanel/observeProteinHighlights.test.ts +27 -3
- package/src/MsaViewPanel/processInit.test.ts +84 -0
- package/src/MsaViewPanel/resolveConnectedTranscript.ts +150 -0
- package/src/MsaViewPanel/runLaunch.test.ts +208 -0
- package/src/MsaViewPanel/runLaunch.ts +141 -0
- package/src/MsaViewPanel/storedData.test.ts +196 -0
- package/src/MsaViewPanel/structureConnection.ts +0 -26
- package/src/MsaViewPanel/util.ts +75 -0
- package/src/utils/blastCache.test.ts +86 -0
- package/src/utils/blastCache.ts +34 -1
- package/src/utils/browserAlign.test.ts +102 -0
- package/src/utils/browserAlign.ts +352 -0
- package/src/utils/ebiBlast.ts +42 -2
- package/src/utils/ebiJobDispatcher.ts +18 -3
- package/src/utils/fetch.ts +29 -2
- package/src/utils/homologSearch.ts +51 -0
- package/src/utils/msa.ts +25 -39
- package/src/utils/msaRows.ts +20 -16
- package/src/utils/ncbiDomains.ts +4 -2
- package/src/utils/ncbiOrthologs.test.ts +25 -0
- package/src/utils/ncbiOrthologs.ts +27 -7
- package/src/utils/pantherOrthologs.ts +6 -11
- package/src/utils/phmmer.ts +62 -12
- package/src/utils/poll.ts +8 -1
- package/src/utils/unirefHomologs.test.ts +149 -0
- package/src/utils/unirefHomologs.ts +323 -0
- package/src/version.ts +1 -1
- package/dist/MsaViewPanel/structureConnection.test.js +0 -53
- package/src/MsaViewPanel/structureConnection.test.ts +0 -62
- /package/dist/{MsaViewPanel/structureConnection.test.d.ts → LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.d.ts} +0 -0
|
@@ -33,10 +33,24 @@ const setQuerySeqName = vi.fn();
|
|
|
33
33
|
function makeModel(orthologParams) {
|
|
34
34
|
return {
|
|
35
35
|
orthologParams,
|
|
36
|
-
setProgress: () => { },
|
|
37
36
|
setQuerySeqName,
|
|
38
37
|
};
|
|
39
38
|
}
|
|
39
|
+
// a scope that never cancels, so these tests see the launch's own behaviour --
|
|
40
|
+
// what the scope does when it IS cancelled is runLaunch.test.ts's subject
|
|
41
|
+
function launch({ self }) {
|
|
42
|
+
return doLaunchOrthologs({
|
|
43
|
+
self,
|
|
44
|
+
scope: {
|
|
45
|
+
signal: new AbortController().signal,
|
|
46
|
+
act: fn => {
|
|
47
|
+
fn();
|
|
48
|
+
},
|
|
49
|
+
onProgress: () => { },
|
|
50
|
+
onRid: () => { },
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
40
54
|
function params(extra = {}) {
|
|
41
55
|
return {
|
|
42
56
|
taxId: HUMAN,
|
|
@@ -79,11 +93,11 @@ beforeEach(() => {
|
|
|
79
93
|
});
|
|
80
94
|
describe('which species become rows', () => {
|
|
81
95
|
test('omitted taxa asks for no restriction at all, which is every ortholog NCBI has', async () => {
|
|
82
|
-
await
|
|
96
|
+
await launch({ self: makeModel(params()) });
|
|
83
97
|
expect(rowRequest().taxa).toBeUndefined();
|
|
84
98
|
});
|
|
85
99
|
test('given taxa is taken as written', async () => {
|
|
86
|
-
await
|
|
100
|
+
await launch({
|
|
87
101
|
self: makeModel(params({ taxa: [HUMAN, 10090, 9615] })),
|
|
88
102
|
});
|
|
89
103
|
expect(rowRequest().taxa).toEqual([9606, 9615, 10090]);
|
|
@@ -92,20 +106,20 @@ describe('which species become rows', () => {
|
|
|
92
106
|
// and "the query row already covers this one" stay separable -- an unrestricted
|
|
93
107
|
// launch still has to drop the query species.
|
|
94
108
|
test('the query species is excluded whether or not taxa was given', async () => {
|
|
95
|
-
await
|
|
109
|
+
await launch({ self: makeModel(params()) });
|
|
96
110
|
expect(rowRequest().exclude).toBe(HUMAN);
|
|
97
111
|
vi.clearAllMocks();
|
|
98
112
|
mockResolveGeneId.mockResolvedValue({ geneId: GENE_ID, matched: 'NLRP1' });
|
|
99
113
|
mockFetchProtein.mockResolvedValue(REPRESENTATIVE);
|
|
100
114
|
mockFetchRows.mockResolvedValue([]);
|
|
101
115
|
mockLaunchMSA.mockResolvedValue({ msa: '', tree: '' });
|
|
102
|
-
await
|
|
116
|
+
await launch({
|
|
103
117
|
self: makeModel(params({ taxa: [HUMAN, 10090] })),
|
|
104
118
|
});
|
|
105
119
|
expect(rowRequest().exclude).toBe(HUMAN);
|
|
106
120
|
});
|
|
107
121
|
test('an empty list is a request for no rows, not a request for all of them', async () => {
|
|
108
|
-
await
|
|
122
|
+
await launch({ self: makeModel(params({ taxa: [] })) });
|
|
109
123
|
expect(rowRequest().taxa).toEqual([]);
|
|
110
124
|
});
|
|
111
125
|
});
|
|
@@ -114,11 +128,11 @@ describe('which species become rows', () => {
|
|
|
114
128
|
// second a row.
|
|
115
129
|
describe('the row cap', () => {
|
|
116
130
|
test('is passed through when given', async () => {
|
|
117
|
-
await
|
|
131
|
+
await launch({ self: makeModel(params({ maxSpecies: 12 })) });
|
|
118
132
|
expect(rowRequest().limit).toBe(12);
|
|
119
133
|
});
|
|
120
134
|
test('omitted leaves the default to fetchOrthologGenes rather than sending Infinity', async () => {
|
|
121
|
-
await
|
|
135
|
+
await launch({ self: makeModel(params()) });
|
|
122
136
|
expect(rowRequest().limit).toBeUndefined();
|
|
123
137
|
expect(defaultMaxSpecies).toBeGreaterThan(2);
|
|
124
138
|
});
|
|
@@ -129,7 +143,7 @@ describe('the row cap', () => {
|
|
|
129
143
|
// model's querySeqName and the header have to be the same string.
|
|
130
144
|
describe('the query row name', () => {
|
|
131
145
|
test('is the species, marked, rather than a bare QUERY among named rows', async () => {
|
|
132
|
-
await
|
|
146
|
+
await launch({ self: makeModel(params()) });
|
|
133
147
|
expect(queryRowName()).toBe('human_query');
|
|
134
148
|
expect(setQuerySeqName).toHaveBeenCalledWith('human_query');
|
|
135
149
|
});
|
|
@@ -137,41 +151,41 @@ describe('the query row name', () => {
|
|
|
137
151
|
mockFetchRows.mockResolvedValue([
|
|
138
152
|
{ label: 'human_query', sequence: 'MM' },
|
|
139
153
|
]);
|
|
140
|
-
await
|
|
154
|
+
await launch({ self: makeModel(params()) });
|
|
141
155
|
expect(queryRowName()).toBe('human_query_2');
|
|
142
156
|
expect(setQuerySeqName).toHaveBeenCalledWith('human_query_2');
|
|
143
157
|
});
|
|
144
158
|
test('falls back rather than throwing when NCBI cannot name the taxon', async () => {
|
|
145
159
|
vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
146
160
|
mockFetchTaxonomy.mockRejectedValue(new Error('429'));
|
|
147
|
-
await
|
|
161
|
+
await launch({ self: makeModel(params()) });
|
|
148
162
|
expect(queryRowName()).toBe('query_query');
|
|
149
163
|
});
|
|
150
164
|
test('the metadata that drives the domain overlay is keyed to that same name', async () => {
|
|
151
|
-
const result = await
|
|
165
|
+
const result = await launch({ self: makeModel(params()) });
|
|
152
166
|
expect(Object.keys(JSON.parse(result.treeMetadata))).toContain('human_query');
|
|
153
167
|
});
|
|
154
168
|
});
|
|
155
169
|
describe('the query row sequence', () => {
|
|
156
170
|
test('omitted proteinSequence falls back to the representative protein', async () => {
|
|
157
|
-
await
|
|
171
|
+
await launch({ self: makeModel(params()) });
|
|
158
172
|
expect(queryRowSent()).toBe(REPRESENTATIVE.sequence);
|
|
159
173
|
});
|
|
160
174
|
test('a supplied sequence is used, and is cleaned first', async () => {
|
|
161
|
-
await
|
|
175
|
+
await launch({
|
|
162
176
|
self: makeModel(params({ proteinSequence: 'MAGG*AWGR&' })),
|
|
163
177
|
});
|
|
164
178
|
expect(queryRowSent()).toBe('MAGGAWGR');
|
|
165
179
|
});
|
|
166
180
|
test('throws when neither a sequence nor a representative is available', async () => {
|
|
167
181
|
mockFetchProtein.mockResolvedValue(undefined);
|
|
168
|
-
await expect(
|
|
182
|
+
await expect(launch({ self: makeModel(params()) })).rejects.toThrow(/No query protein/);
|
|
169
183
|
expect(mockLaunchMSA).not.toHaveBeenCalled();
|
|
170
184
|
});
|
|
171
185
|
test('a failed representative lookup does not take down a launch that brought its own sequence', async () => {
|
|
172
186
|
vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
173
187
|
mockFetchProtein.mockRejectedValue(new Error('429'));
|
|
174
|
-
await
|
|
188
|
+
await launch({
|
|
175
189
|
self: makeModel(params({ proteinSequence: REPRESENTATIVE.sequence })),
|
|
176
190
|
});
|
|
177
191
|
expect(queryRowSent()).toBe(REPRESENTATIVE.sequence);
|
|
@@ -183,14 +197,24 @@ describe('the query row sequence', () => {
|
|
|
183
197
|
// here, in both directions.
|
|
184
198
|
describe('the Accession that drives the domain overlay', () => {
|
|
185
199
|
test('is attached when the query row IS the representative protein', async () => {
|
|
186
|
-
const result = await
|
|
200
|
+
const result = await launch({ self: makeModel(params()) });
|
|
187
201
|
expect(queryMetadata(result)).toMatchObject({
|
|
188
202
|
'Gene ID': GENE_ID,
|
|
189
203
|
Accession: REPRESENTATIVE.accession,
|
|
190
204
|
});
|
|
191
205
|
});
|
|
206
|
+
// orthologParams is frozen, so a session reload hands the transcript back as
|
|
207
|
+
// the plain JSON it serialized to; .get() on that threw
|
|
208
|
+
test('names the transcript from a snapshot-restored transcript', async () => {
|
|
209
|
+
const result = await launch({
|
|
210
|
+
self: makeModel(params({
|
|
211
|
+
selectedTranscript: { uniqueId: 'NM_1', name: 'TP53-201' },
|
|
212
|
+
})),
|
|
213
|
+
});
|
|
214
|
+
expect(queryMetadata(result).Transcript).toBe('TP53-201');
|
|
215
|
+
});
|
|
192
216
|
test('is withheld from a non-representative isoform', async () => {
|
|
193
|
-
const result = await
|
|
217
|
+
const result = await launch({
|
|
194
218
|
self: makeModel(params({ proteinSequence: 'MDIFFERENTISOFORM' })),
|
|
195
219
|
});
|
|
196
220
|
expect(queryMetadata(result).Accession).toBeUndefined();
|
|
@@ -198,7 +222,7 @@ describe('the Accession that drives the domain overlay', () => {
|
|
|
198
222
|
test('is withheld when the representative lookup failed', async () => {
|
|
199
223
|
vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
200
224
|
mockFetchProtein.mockRejectedValue(new Error('429'));
|
|
201
|
-
const result = await
|
|
225
|
+
const result = await launch({
|
|
202
226
|
self: makeModel(params({ proteinSequence: REPRESENTATIVE.sequence })),
|
|
203
227
|
});
|
|
204
228
|
expect(queryMetadata(result).Accession).toBeUndefined();
|
|
@@ -235,12 +259,12 @@ describe('the PANTHER source', () => {
|
|
|
235
259
|
mockFetchTaxonomy.mockResolvedValue(new Map([[YEAST, { sciname: 'Saccharomyces cerevisiae' }]]));
|
|
236
260
|
});
|
|
237
261
|
test('source omitted is NCBI, so an old launch never reaches PANTHER', async () => {
|
|
238
|
-
await
|
|
262
|
+
await launch({ self: makeModel(params()) });
|
|
239
263
|
expect(mockFetchPanther).not.toHaveBeenCalled();
|
|
240
264
|
expect(mockResolveGeneId).toHaveBeenCalled();
|
|
241
265
|
});
|
|
242
266
|
test('source panther asks PANTHER with the same species semantics, and skips NCBI', async () => {
|
|
243
|
-
await
|
|
267
|
+
await launch({
|
|
244
268
|
self: makeModel({
|
|
245
269
|
taxId: YEAST,
|
|
246
270
|
source: 'panther',
|
|
@@ -260,7 +284,7 @@ describe('the PANTHER source', () => {
|
|
|
260
284
|
expect(limit).toBe(7);
|
|
261
285
|
});
|
|
262
286
|
test("the query row is PANTHER's own entry for the gene when no sequence was supplied, and carries its UniProt accession for the domain overlay", async () => {
|
|
263
|
-
const result = await
|
|
287
|
+
const result = await launch({
|
|
264
288
|
self: makeModel({
|
|
265
289
|
taxId: YEAST,
|
|
266
290
|
source: 'panther',
|
|
@@ -280,7 +304,7 @@ describe('the PANTHER source', () => {
|
|
|
280
304
|
});
|
|
281
305
|
});
|
|
282
306
|
test('a supplied sequence still wins, and a different isoform earns no Accession', async () => {
|
|
283
|
-
const result = await
|
|
307
|
+
const result = await launch({
|
|
284
308
|
self: makeModel({
|
|
285
309
|
taxId: YEAST,
|
|
286
310
|
source: 'panther',
|
|
@@ -294,7 +318,7 @@ describe('the PANTHER source', () => {
|
|
|
294
318
|
});
|
|
295
319
|
test('names PANTHER when it has no protein for the query row', async () => {
|
|
296
320
|
mockFetchPanther.mockResolvedValue({ ...found, query: undefined });
|
|
297
|
-
await expect(
|
|
321
|
+
await expect(launch({
|
|
298
322
|
self: makeModel({
|
|
299
323
|
taxId: YEAST,
|
|
300
324
|
source: 'panther',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getSession } from '@jbrowse/core/util';
|
|
2
|
-
import { hasHoverPosition, hasQueryRow } from './util';
|
|
2
|
+
import { hasHoverPosition, hasQueryRow, transcriptPosToVisibleCol, } from './util';
|
|
3
3
|
export function genomeToMSA({ model }) {
|
|
4
4
|
const { hovered } = getSession(model);
|
|
5
5
|
const { querySeqName, transcriptToMsaMap, connectedView, mafRegion } = model;
|
|
@@ -26,9 +26,9 @@ export function genomeToMSA({ model }) {
|
|
|
26
26
|
// without it the same numeric coordinate on an unrelated chromosome matches a
|
|
27
27
|
// g2p key and lights up a column for a different locus
|
|
28
28
|
if (refName === transcriptToMsaMap?.refName) {
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
31
|
-
return model
|
|
29
|
+
const proteinPos = transcriptToMsaMap.g2p[genomePos];
|
|
30
|
+
if (proteinPos !== undefined) {
|
|
31
|
+
return transcriptPosToVisibleCol(model, proteinPos);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
return undefined;
|
|
@@ -202,6 +202,7 @@ describe('genomeToMSA', () => {
|
|
|
202
202
|
const mockSeqPosToVisibleCol = vi.fn().mockReturnValue(10);
|
|
203
203
|
const model = {
|
|
204
204
|
querySeqName: 'QUERY',
|
|
205
|
+
querySeqOffset: 0,
|
|
205
206
|
rows: [['QUERY', 'MKVLTAEEK']],
|
|
206
207
|
transcriptToMsaMap: {
|
|
207
208
|
refName: 'chr1',
|
|
@@ -211,6 +212,7 @@ describe('genomeToMSA', () => {
|
|
|
211
212
|
mafRegion: undefined,
|
|
212
213
|
connectedView: { initialized: true },
|
|
213
214
|
seqPosToVisibleCol: mockSeqPosToVisibleCol,
|
|
215
|
+
visibleColToSeqPos: (_name, col) => col,
|
|
214
216
|
};
|
|
215
217
|
const result = genomeToMSA({ model });
|
|
216
218
|
expect(mockSeqPosToVisibleCol).toHaveBeenCalledWith('QUERY', 10);
|
|
@@ -240,6 +242,71 @@ describe('genomeToMSA', () => {
|
|
|
240
242
|
expect(genomeToMSA({ model })).toBeUndefined();
|
|
241
243
|
expect(mockSeqPosToVisibleCol).not.toHaveBeenCalled();
|
|
242
244
|
});
|
|
245
|
+
// a pasted alignment often carries only the region BLAST aligned, and
|
|
246
|
+
// querySeqOffset says how much of the protein comes before it
|
|
247
|
+
test('shifts a trimmed query row by querySeqOffset', () => {
|
|
248
|
+
mockGetSession.mockReturnValue({
|
|
249
|
+
hovered: {
|
|
250
|
+
hoverFeature: {},
|
|
251
|
+
hoverPosition: { coord: 1005, refName: 'chr1' },
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
const seqPosToVisibleCol = vi.fn((_name, pos) => pos);
|
|
255
|
+
const model = {
|
|
256
|
+
querySeqName: 'QUERY',
|
|
257
|
+
querySeqOffset: 4,
|
|
258
|
+
rows: [['QUERY', 'MKVLTAEEK']],
|
|
259
|
+
transcriptToMsaMap: { refName: 'chr1', g2p: { 1004: 10 } },
|
|
260
|
+
mafRegion: undefined,
|
|
261
|
+
connectedView: { initialized: true },
|
|
262
|
+
seqPosToVisibleCol,
|
|
263
|
+
visibleColToSeqPos: (_name, col) => col,
|
|
264
|
+
};
|
|
265
|
+
expect(genomeToMSA({ model })).toBe(6);
|
|
266
|
+
expect(seqPosToVisibleCol).toHaveBeenCalledWith('QUERY', 6);
|
|
267
|
+
});
|
|
268
|
+
test('maps nothing for a residue before the trimmed row starts', () => {
|
|
269
|
+
mockGetSession.mockReturnValue({
|
|
270
|
+
hovered: {
|
|
271
|
+
hoverFeature: {},
|
|
272
|
+
hoverPosition: { coord: 1005, refName: 'chr1' },
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
const seqPosToVisibleCol = vi.fn((_name, pos) => pos);
|
|
276
|
+
const model = {
|
|
277
|
+
querySeqName: 'QUERY',
|
|
278
|
+
querySeqOffset: 20,
|
|
279
|
+
rows: [['QUERY', 'MKVLTAEEK']],
|
|
280
|
+
transcriptToMsaMap: { refName: 'chr1', g2p: { 1004: 10 } },
|
|
281
|
+
mafRegion: undefined,
|
|
282
|
+
connectedView: { initialized: true },
|
|
283
|
+
seqPosToVisibleCol,
|
|
284
|
+
visibleColToSeqPos: (_name, col) => col,
|
|
285
|
+
};
|
|
286
|
+
expect(genomeToMSA({ model })).toBeUndefined();
|
|
287
|
+
expect(seqPosToVisibleCol).not.toHaveBeenCalled();
|
|
288
|
+
});
|
|
289
|
+
// react-msaview answers one column past the end for a residue the row does
|
|
290
|
+
// not have, which would light the last column for everything beyond it
|
|
291
|
+
test('maps nothing for a residue the row stops short of', () => {
|
|
292
|
+
mockGetSession.mockReturnValue({
|
|
293
|
+
hovered: {
|
|
294
|
+
hoverFeature: {},
|
|
295
|
+
hoverPosition: { coord: 1005, refName: 'chr1' },
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
const model = {
|
|
299
|
+
querySeqName: 'QUERY',
|
|
300
|
+
querySeqOffset: 0,
|
|
301
|
+
rows: [['QUERY', 'MKV']],
|
|
302
|
+
transcriptToMsaMap: { refName: 'chr1', g2p: { 1004: 10 } },
|
|
303
|
+
mafRegion: undefined,
|
|
304
|
+
connectedView: { initialized: true },
|
|
305
|
+
seqPosToVisibleCol: () => 3,
|
|
306
|
+
visibleColToSeqPos: () => undefined,
|
|
307
|
+
};
|
|
308
|
+
expect(genomeToMSA({ model })).toBeUndefined();
|
|
309
|
+
});
|
|
243
310
|
test('returns undefined when g2p has no mapping for coord', () => {
|
|
244
311
|
mockGetSession.mockReturnValue({
|
|
245
312
|
hovered: {
|
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Annotation } from 'react-msaview';
|
|
2
2
|
interface DomainModel {
|
|
3
3
|
data: {
|
|
4
4
|
treeMetadata?: string;
|
|
5
5
|
};
|
|
6
6
|
setProgress: (arg: string) => void;
|
|
7
|
-
|
|
7
|
+
setAnnotations: (annotations: Annotation[]) => void;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
10
|
* Overlay protein domains on the alignment using NCBI's pre-computed CDD
|
|
11
11
|
* annotations. The BLAST workflow stores each hit's accession in treeMetadata,
|
|
12
12
|
* so we look those up via efetch and key the results by MSA row name (which is
|
|
13
13
|
* what react-msaview matches domains against).
|
|
14
|
+
*
|
|
15
|
+
* The overlay is handed over as `Annotation[]`, the shape every source flattens
|
|
16
|
+
* to. It used to be dressed up as an InterProScan response — an `xref` invented
|
|
17
|
+
* to carry the row name — so that `setDomains` could unwrap it again; that entry
|
|
18
|
+
* point exists for plugins holding the actual EBI wire format, which this is
|
|
19
|
+
* not.
|
|
14
20
|
*/
|
|
15
21
|
export declare function loadProteinDomains(self: DomainModel): Promise<void>;
|
|
16
22
|
export {};
|
|
@@ -4,6 +4,12 @@ import { fetchProteinDomains } from '../utils/ncbiDomains';
|
|
|
4
4
|
* annotations. The BLAST workflow stores each hit's accession in treeMetadata,
|
|
5
5
|
* so we look those up via efetch and key the results by MSA row name (which is
|
|
6
6
|
* what react-msaview matches domains against).
|
|
7
|
+
*
|
|
8
|
+
* The overlay is handed over as `Annotation[]`, the shape every source flattens
|
|
9
|
+
* to. It used to be dressed up as an InterProScan response — an `xref` invented
|
|
10
|
+
* to carry the row name — so that `setDomains` could unwrap it again; that entry
|
|
11
|
+
* point exists for plugins holding the actual EBI wire format, which this is
|
|
12
|
+
* not.
|
|
7
13
|
*/
|
|
8
14
|
export async function loadProteinDomains(self) {
|
|
9
15
|
const metadataJson = self.data.treeMetadata;
|
|
@@ -19,15 +25,18 @@ export async function loadProteinDomains(self) {
|
|
|
19
25
|
}
|
|
20
26
|
self.setProgress(`Fetching protein domains from NCBI for ${rowAccessions.length} sequences...`);
|
|
21
27
|
const byAccession = await fetchProteinDomains(rowAccessions.map(r => r.accession));
|
|
22
|
-
const annotations = {}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
const annotations = rowAccessions.flatMap(({ rowName, accession }) => (byAccession.get(accession) ?? []).flatMap(({ signature, locations }) => signature.entry
|
|
29
|
+
? locations.map(({ start, end }) => ({
|
|
30
|
+
id: rowName,
|
|
31
|
+
accession: signature.entry.accession,
|
|
32
|
+
name: signature.entry.name,
|
|
33
|
+
description: signature.entry.description,
|
|
34
|
+
start,
|
|
35
|
+
end,
|
|
36
|
+
}))
|
|
37
|
+
: []));
|
|
38
|
+
if (annotations.length === 0) {
|
|
30
39
|
throw new Error('No CDD domain annotations found for these proteins');
|
|
31
40
|
}
|
|
32
|
-
self.
|
|
41
|
+
self.setAnnotations(annotations);
|
|
33
42
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { expect, test, vi } from 'vitest';
|
|
2
|
+
import { fetchProteinDomains } from '../utils/ncbiDomains';
|
|
3
|
+
import { loadProteinDomains } from './loadProteinDomains';
|
|
4
|
+
vi.mock('../utils/ncbiDomains', () => ({ fetchProteinDomains: vi.fn() }));
|
|
5
|
+
const MATCH = {
|
|
6
|
+
signature: {
|
|
7
|
+
entry: { accession: 'cd00093', name: 'HTH_XRE', description: 'a domain' },
|
|
8
|
+
},
|
|
9
|
+
locations: [{ start: 5, end: 40 }],
|
|
10
|
+
};
|
|
11
|
+
function model(treeMetadata) {
|
|
12
|
+
return {
|
|
13
|
+
data: { treeMetadata: JSON.stringify(treeMetadata) },
|
|
14
|
+
setProgress: vi.fn(),
|
|
15
|
+
setAnnotations: vi.fn(),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
test('an accession row becomes one annotation per location, keyed by row name', async () => {
|
|
19
|
+
vi.mocked(fetchProteinDomains).mockResolvedValue(new Map([['NP_1', [MATCH]]]));
|
|
20
|
+
const self = model({
|
|
21
|
+
QUERY: { Accession: 'NP_1' },
|
|
22
|
+
other: { 'Scientific name': 'Mus musculus' },
|
|
23
|
+
});
|
|
24
|
+
await loadProteinDomains(self);
|
|
25
|
+
expect(self.setAnnotations).toHaveBeenCalledWith([
|
|
26
|
+
{
|
|
27
|
+
id: 'QUERY',
|
|
28
|
+
accession: 'cd00093',
|
|
29
|
+
name: 'HTH_XRE',
|
|
30
|
+
description: 'a domain',
|
|
31
|
+
start: 5,
|
|
32
|
+
end: 40,
|
|
33
|
+
},
|
|
34
|
+
]);
|
|
35
|
+
});
|
|
36
|
+
test('says so rather than clearing the overlay when NCBI has nothing', async () => {
|
|
37
|
+
vi.mocked(fetchProteinDomains).mockResolvedValue(new Map());
|
|
38
|
+
const self = model({ QUERY: { Accession: 'NP_1' } });
|
|
39
|
+
await expect(loadProteinDomains(self)).rejects.toThrow('No CDD domain annotations found');
|
|
40
|
+
expect(self.setAnnotations).not.toHaveBeenCalled();
|
|
41
|
+
});
|