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.
Files changed (171) hide show
  1. package/README.md +7 -0
  2. package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +23 -17
  3. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -0
  4. package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +5 -3
  5. package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +24 -4
  6. package/dist/LaunchMsaView/components/BlastQuery/consts.js +44 -2
  7. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.d.ts +7 -0
  8. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.js +42 -0
  9. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.js +32 -0
  10. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +1 -0
  11. package/dist/LaunchMsaView/components/ManualMSALoader/launchView.d.ts +3 -1
  12. package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js +2 -1
  13. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +10 -4
  14. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +4 -1
  15. package/dist/LaunchMsaView/components/QueryRowSelector.js +21 -5
  16. package/dist/LaunchMsaView/detectQueryRow.d.ts +11 -0
  17. package/dist/LaunchMsaView/detectQueryRow.js +11 -4
  18. package/dist/LaunchMsaView/detectQueryRow.test.js +30 -0
  19. package/dist/LaunchMsaView/useQueryRowName.d.ts +1 -0
  20. package/dist/LaunchMsaView/useQueryRowName.js +5 -1
  21. package/dist/LaunchMsaView/useQueryRowName.test.d.ts +1 -0
  22. package/dist/LaunchMsaView/useQueryRowName.test.js +23 -0
  23. package/dist/LaunchMsaViewExtensionPoint/index.js +19 -12
  24. package/dist/LaunchMsaViewExtensionPoint/index.test.d.ts +1 -0
  25. package/dist/LaunchMsaViewExtensionPoint/index.test.js +64 -0
  26. package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +16 -0
  27. package/dist/MsaViewPanel/afterCreateAutoruns.js +138 -86
  28. package/dist/MsaViewPanel/components/LaunchProgress.js +23 -5
  29. package/dist/MsaViewPanel/components/MsaViewPanel.js +6 -2
  30. package/dist/MsaViewPanel/components/MsaViewPanel.test.js +40 -0
  31. package/dist/MsaViewPanel/doLaunchBlast.d.ts +11 -1
  32. package/dist/MsaViewPanel/doLaunchBlast.js +79 -96
  33. package/dist/MsaViewPanel/doLaunchBlast.test.d.ts +1 -0
  34. package/dist/MsaViewPanel/doLaunchBlast.test.js +158 -0
  35. package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +3 -1
  36. package/dist/MsaViewPanel/doLaunchOrthologs.js +46 -10
  37. package/dist/MsaViewPanel/doLaunchOrthologs.test.js +48 -24
  38. package/dist/MsaViewPanel/genomeToMSA.js +4 -4
  39. package/dist/MsaViewPanel/genomeToMSA.test.js +67 -0
  40. package/dist/MsaViewPanel/loadProteinDomains.d.ts +8 -2
  41. package/dist/MsaViewPanel/loadProteinDomains.js +18 -9
  42. package/dist/MsaViewPanel/loadProteinDomains.test.d.ts +1 -0
  43. package/dist/MsaViewPanel/loadProteinDomains.test.js +41 -0
  44. package/dist/MsaViewPanel/model.d.ts +306 -85
  45. package/dist/MsaViewPanel/model.js +169 -42
  46. package/dist/MsaViewPanel/model.test.d.ts +1 -0
  47. package/dist/MsaViewPanel/model.test.js +100 -0
  48. package/dist/MsaViewPanel/msaCoordToGenomeCoord.d.ts +12 -1
  49. package/dist/MsaViewPanel/msaCoordToGenomeCoord.js +8 -8
  50. package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.js +133 -32
  51. package/dist/MsaViewPanel/msaDataStore.d.ts +7 -3
  52. package/dist/MsaViewPanel/msaDataStore.js +26 -7
  53. package/dist/MsaViewPanel/msaDataStore.test.d.ts +1 -0
  54. package/dist/MsaViewPanel/msaDataStore.test.js +44 -0
  55. package/dist/MsaViewPanel/observeProteinHighlights.test.js +20 -3
  56. package/dist/MsaViewPanel/processInit.test.d.ts +1 -0
  57. package/dist/MsaViewPanel/processInit.test.js +72 -0
  58. package/dist/MsaViewPanel/resolveConnectedTranscript.d.ts +9 -0
  59. package/dist/MsaViewPanel/resolveConnectedTranscript.js +122 -0
  60. package/dist/MsaViewPanel/runLaunch.d.ts +39 -0
  61. package/dist/MsaViewPanel/runLaunch.js +100 -0
  62. package/dist/MsaViewPanel/runLaunch.test.d.ts +1 -0
  63. package/dist/MsaViewPanel/runLaunch.test.js +175 -0
  64. package/dist/MsaViewPanel/storedData.test.d.ts +1 -0
  65. package/dist/MsaViewPanel/storedData.test.js +160 -0
  66. package/dist/MsaViewPanel/structureConnection.d.ts +0 -4
  67. package/dist/MsaViewPanel/structureConnection.js +0 -19
  68. package/dist/MsaViewPanel/util.d.ts +39 -0
  69. package/dist/MsaViewPanel/util.js +44 -0
  70. package/dist/jbrowse-plugin-msaview.umd.production.min.js +46 -45
  71. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  72. package/dist/utils/blastCache.js +32 -0
  73. package/dist/utils/blastCache.test.d.ts +1 -0
  74. package/dist/utils/blastCache.test.js +72 -0
  75. package/dist/utils/browserAlign.d.ts +42 -0
  76. package/dist/utils/browserAlign.js +297 -0
  77. package/dist/utils/browserAlign.test.d.ts +1 -0
  78. package/dist/utils/browserAlign.test.js +85 -0
  79. package/dist/utils/ebiBlast.d.ts +13 -2
  80. package/dist/utils/ebiBlast.js +32 -4
  81. package/dist/utils/ebiJobDispatcher.d.ts +6 -3
  82. package/dist/utils/ebiJobDispatcher.js +13 -6
  83. package/dist/utils/fetch.d.ts +8 -1
  84. package/dist/utils/fetch.js +28 -2
  85. package/dist/utils/homologSearch.d.ts +31 -0
  86. package/dist/utils/homologSearch.js +6 -0
  87. package/dist/utils/msa.d.ts +5 -11
  88. package/dist/utils/msa.js +19 -30
  89. package/dist/utils/msaRows.d.ts +11 -8
  90. package/dist/utils/msaRows.js +14 -12
  91. package/dist/utils/ncbiDomains.d.ts +2 -2
  92. package/dist/utils/ncbiOrthologs.d.ts +11 -1
  93. package/dist/utils/ncbiOrthologs.js +26 -7
  94. package/dist/utils/ncbiOrthologs.test.js +23 -1
  95. package/dist/utils/pantherOrthologs.js +2 -10
  96. package/dist/utils/phmmer.d.ts +12 -1
  97. package/dist/utils/phmmer.js +52 -13
  98. package/dist/utils/poll.d.ts +6 -1
  99. package/dist/utils/poll.js +7 -2
  100. package/dist/utils/unirefHomologs.d.ts +94 -0
  101. package/dist/utils/unirefHomologs.js +193 -0
  102. package/dist/utils/unirefHomologs.test.d.ts +1 -0
  103. package/dist/utils/unirefHomologs.test.js +118 -0
  104. package/dist/version.d.ts +1 -1
  105. package/dist/version.js +1 -1
  106. package/package.json +17 -16
  107. package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +40 -19
  108. package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -0
  109. package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +7 -2
  110. package/src/LaunchMsaView/components/BlastQuery/consts.ts +50 -2
  111. package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.ts +43 -0
  112. package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.ts +64 -0
  113. package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -0
  114. package/src/LaunchMsaView/components/ManualMSALoader/launchView.ts +4 -0
  115. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +12 -5
  116. package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +4 -1
  117. package/src/LaunchMsaView/components/QueryRowSelector.tsx +35 -7
  118. package/src/LaunchMsaView/detectQueryRow.test.ts +34 -0
  119. package/src/LaunchMsaView/detectQueryRow.ts +22 -4
  120. package/src/LaunchMsaView/useQueryRowName.test.ts +27 -0
  121. package/src/LaunchMsaView/useQueryRowName.ts +5 -1
  122. package/src/LaunchMsaViewExtensionPoint/index.test.ts +74 -0
  123. package/src/LaunchMsaViewExtensionPoint/index.ts +53 -12
  124. package/src/MsaViewPanel/afterCreateAutoruns.ts +150 -81
  125. package/src/MsaViewPanel/components/LaunchProgress.tsx +46 -4
  126. package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +43 -0
  127. package/src/MsaViewPanel/components/MsaViewPanel.tsx +8 -2
  128. package/src/MsaViewPanel/doLaunchBlast.test.ts +207 -0
  129. package/src/MsaViewPanel/doLaunchBlast.ts +94 -140
  130. package/src/MsaViewPanel/doLaunchOrthologs.test.ts +54 -26
  131. package/src/MsaViewPanel/doLaunchOrthologs.ts +63 -9
  132. package/src/MsaViewPanel/genomeToMSA.test.ts +76 -0
  133. package/src/MsaViewPanel/genomeToMSA.ts +8 -4
  134. package/src/MsaViewPanel/loadProteinDomains.test.ts +52 -0
  135. package/src/MsaViewPanel/loadProteinDomains.ts +24 -11
  136. package/src/MsaViewPanel/model.test.ts +121 -0
  137. package/src/MsaViewPanel/model.ts +200 -49
  138. package/src/MsaViewPanel/msaCoordToGenomeCoord.test.ts +152 -32
  139. package/src/MsaViewPanel/msaCoordToGenomeCoord.ts +21 -11
  140. package/src/MsaViewPanel/msaDataStore.test.ts +54 -0
  141. package/src/MsaViewPanel/msaDataStore.ts +32 -13
  142. package/src/MsaViewPanel/observeProteinHighlights.test.ts +27 -3
  143. package/src/MsaViewPanel/processInit.test.ts +84 -0
  144. package/src/MsaViewPanel/resolveConnectedTranscript.ts +150 -0
  145. package/src/MsaViewPanel/runLaunch.test.ts +208 -0
  146. package/src/MsaViewPanel/runLaunch.ts +141 -0
  147. package/src/MsaViewPanel/storedData.test.ts +196 -0
  148. package/src/MsaViewPanel/structureConnection.ts +0 -26
  149. package/src/MsaViewPanel/util.ts +75 -0
  150. package/src/utils/blastCache.test.ts +86 -0
  151. package/src/utils/blastCache.ts +34 -1
  152. package/src/utils/browserAlign.test.ts +102 -0
  153. package/src/utils/browserAlign.ts +352 -0
  154. package/src/utils/ebiBlast.ts +42 -2
  155. package/src/utils/ebiJobDispatcher.ts +18 -3
  156. package/src/utils/fetch.ts +29 -2
  157. package/src/utils/homologSearch.ts +51 -0
  158. package/src/utils/msa.ts +25 -39
  159. package/src/utils/msaRows.ts +20 -16
  160. package/src/utils/ncbiDomains.ts +4 -2
  161. package/src/utils/ncbiOrthologs.test.ts +25 -0
  162. package/src/utils/ncbiOrthologs.ts +27 -7
  163. package/src/utils/pantherOrthologs.ts +6 -11
  164. package/src/utils/phmmer.ts +62 -12
  165. package/src/utils/poll.ts +8 -1
  166. package/src/utils/unirefHomologs.test.ts +149 -0
  167. package/src/utils/unirefHomologs.ts +323 -0
  168. package/src/version.ts +1 -1
  169. package/dist/MsaViewPanel/structureConnection.test.js +0 -53
  170. package/src/MsaViewPanel/structureConnection.test.ts +0 -62
  171. /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 doLaunchOrthologs({ self: makeModel(params()) });
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 doLaunchOrthologs({
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 doLaunchOrthologs({ self: makeModel(params()) });
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 doLaunchOrthologs({
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 doLaunchOrthologs({ self: makeModel(params({ taxa: [] })) });
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 doLaunchOrthologs({ self: makeModel(params({ maxSpecies: 12 })) });
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 doLaunchOrthologs({ self: makeModel(params()) });
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 doLaunchOrthologs({ self: makeModel(params()) });
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 doLaunchOrthologs({ self: makeModel(params()) });
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 doLaunchOrthologs({ self: makeModel(params()) });
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 doLaunchOrthologs({ self: makeModel(params()) });
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 doLaunchOrthologs({ self: makeModel(params()) });
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 doLaunchOrthologs({
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(doLaunchOrthologs({ self: makeModel(params()) })).rejects.toThrow(/No query protein/);
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 doLaunchOrthologs({
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 doLaunchOrthologs({ self: makeModel(params()) });
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 doLaunchOrthologs({
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 doLaunchOrthologs({
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 doLaunchOrthologs({ self: makeModel(params()) });
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 doLaunchOrthologs({
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 doLaunchOrthologs({
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 doLaunchOrthologs({
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(doLaunchOrthologs({
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 seqPos = transcriptToMsaMap.g2p[genomePos];
30
- if (seqPos !== undefined) {
31
- return model.seqPosToVisibleCol(querySeqName, seqPos);
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 { InterProScanResults } from 'react-msaview';
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
- setDomains: (data: Record<string, InterProScanResults>) => void;
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
- for (const { rowName, accession } of rowAccessions) {
24
- const matches = byAccession.get(accession);
25
- if (matches && matches.length > 0) {
26
- annotations[rowName] = { matches, xref: [{ id: rowName }] };
27
- }
28
- }
29
- if (Object.keys(annotations).length === 0) {
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.setDomains(annotations);
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
+ });