protvista-uniprot 4.6.1 → 4.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.
@@ -9,14 +9,24 @@ type ProcessedStructureData = {
9
9
  downloadLink?: string;
10
10
  sourceDBLink?: string;
11
11
  protvistaFeatureId: string;
12
+ amAnnotationsUrl?: string;
13
+ isoform?: TemplateResult;
12
14
  };
15
+ type IsoformIdSequence = [
16
+ {
17
+ isoformId: string;
18
+ sequence: string;
19
+ }
20
+ ];
13
21
  declare class ProtvistaUniprotStructure extends LitElement {
14
22
  accession?: string;
15
23
  sequence?: string;
24
+ checksum?: string;
16
25
  data?: ProcessedStructureData[];
17
26
  structureId?: string;
18
27
  metaInfo?: TemplateResult;
19
28
  colorTheme?: string;
29
+ isoforms?: IsoformIdSequence;
20
30
  private loading?;
21
31
  private alphamissenseAvailable?;
22
32
  private modelUrl;
@@ -28,6 +38,9 @@ declare class ProtvistaUniprotStructure extends LitElement {
28
38
  structureId: {
29
39
  type: StringConstructor;
30
40
  };
41
+ checksum: {
42
+ type: StringConstructor;
43
+ };
31
44
  sequence: {
32
45
  type: StringConstructor;
33
46
  };
@@ -43,16 +56,21 @@ declare class ProtvistaUniprotStructure extends LitElement {
43
56
  alphamissenseAvailable: {
44
57
  type: BooleanConstructor;
45
58
  };
59
+ isoforms: {
60
+ type: ObjectConstructor;
61
+ attribute: boolean;
62
+ };
46
63
  };
47
64
  connectedCallback(): Promise<void>;
48
65
  disconnectedCallback(): void;
49
66
  updated(): void;
50
67
  addStyles(): void;
51
68
  removeStyles(): void;
52
- onTableRowClick({ id, source, downloadLink, }: {
69
+ onTableRowClick({ id, source, downloadLink, amAnnotationsUrl, }: {
53
70
  id: string;
54
71
  source?: string;
55
72
  downloadLink?: string;
73
+ amAnnotationsUrl?: string;
56
74
  }): void;
57
75
  get cssStyle(): import('lit').CSSResult;
58
76
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "protvista-uniprot",
3
3
  "description": "ProtVista tool for the UniProt website",
4
- "version": "4.6.1",
4
+ "version": "4.6.3",
5
5
  "files": [
6
6
  "dist",
7
7
  "src"
@@ -19,6 +19,7 @@ const PDBLinks = [
19
19
  ];
20
20
  const alphaFoldLink = 'https://alphafold.ebi.ac.uk/entry/';
21
21
  const foldseekLink = `https://search.foldseek.com/search`;
22
+ const uniprotKBLink = 'https://www.uniprot.org/uniprotkb/';
22
23
 
23
24
  // Excluded sources from 3d-beacons are PDBe and AlphaFold models as we fetch them separately from their respective API's
24
25
  const providersFrom3DBeacons = [
@@ -33,6 +34,7 @@ const providersFrom3DBeacons = [
33
34
  ];
34
35
 
35
36
  const sourceMethods = new Map([
37
+ ['AlphaFold DB', 'Predicted'],
36
38
  ['SWISS-MODEL', 'Modeling'],
37
39
  ['ModelArchive', 'Modeling'],
38
40
  ['PED', 'Modeling'],
@@ -63,7 +65,7 @@ type Sequence = {
63
65
  };
64
66
 
65
67
  type BeaconsData = {
66
- uniprot_entry: {
68
+ uniprot_entry?: {
67
69
  ac: string;
68
70
  id: string;
69
71
  uniprot_checksum: string;
@@ -71,6 +73,11 @@ type BeaconsData = {
71
73
  segment_start: number;
72
74
  segment_end: number;
73
75
  };
76
+ entry?: {
77
+ sequence: string;
78
+ checksum: string;
79
+ checksum_type: string;
80
+ };
74
81
  structures: {
75
82
  summary: {
76
83
  model_identifier: string;
@@ -117,80 +124,153 @@ type ProcessedStructureData = {
117
124
  downloadLink?: string;
118
125
  sourceDBLink?: string;
119
126
  protvistaFeatureId: string;
127
+ amAnnotationsUrl?: string;
128
+ isoform?: TemplateResult;
129
+ };
130
+
131
+ type IsoformIdSequence = [
132
+ {
133
+ isoformId: string;
134
+ sequence: string;
135
+ }
136
+ ];
137
+
138
+ const getIsoformNum = (s) => {
139
+ const match = s.match(/-(\d+)-F1$/);
140
+ return match ? Number(match[1]) : 0;
120
141
  };
121
142
 
122
143
  const processPDBData = (data: UniProtKBData): ProcessedStructureData[] =>
123
- data.uniProtKBCrossReferences ? data.uniProtKBCrossReferences
124
- .filter((xref) => xref.database === 'PDB')
125
- .sort((refA, refB) => refA.id.localeCompare(refB.id))
126
- .map(({ id, properties }) => {
127
- if (!properties) {
128
- return;
129
- }
144
+ data.uniProtKBCrossReferences
145
+ ? data.uniProtKBCrossReferences
146
+ .filter((xref) => xref.database === 'PDB')
147
+ .sort((refA, refB) => refA.id.localeCompare(refB.id))
148
+ .map(({ id, properties }) => {
149
+ if (!properties) {
150
+ return;
151
+ }
152
+
153
+ const propertyMap = properties.reduce((acc, item) => {
154
+ acc[item.key] = item.value;
155
+ return acc;
156
+ }, {} as Record<string, string>);
157
+
158
+ const method = propertyMap['Method'];
159
+ const resolution = propertyMap['Resolution'];
160
+ const chains = propertyMap['Chains'];
161
+
162
+ let chain;
163
+ let positions;
164
+ if (chains) {
165
+ const tokens = chains.split('=');
166
+ if (tokens.length === 2) {
167
+ [chain, positions] = tokens;
168
+ }
169
+ }
170
+ const output: ProcessedStructureData = {
171
+ id,
172
+ source: 'PDB',
173
+ method,
174
+ resolution:
175
+ !resolution || resolution === '-' ? undefined : resolution,
176
+ downloadLink: `https://www.ebi.ac.uk/pdbe/entry-files/download/pdb${id.toLowerCase()}.ent`,
177
+ chain,
178
+ positions,
179
+ protvistaFeatureId: id,
180
+ };
181
+ return output;
182
+ })
183
+ .filter(
184
+ (
185
+ transformedItem: ProcessedStructureData | undefined
186
+ ): transformedItem is ProcessedStructureData =>
187
+ transformedItem !== undefined
188
+ )
189
+ : [];
190
+
191
+ const processAFData = (
192
+ data: AlphaFoldPayload,
193
+ accession?: string,
194
+ isoforms?: IsoformIdSequence,
195
+ canonicalSequence?: string,
196
+ ): ProcessedStructureData[] =>
197
+ data
198
+ .map((d) => {
199
+ const isoformMatch = isoforms?.find(
200
+ ({ sequence }) => d.sequence === sequence
201
+ );
130
202
 
131
- const propertyMap = properties.reduce((acc, item) => {
132
- acc[item.key] = item.value;
133
- return acc;
134
- }, {} as Record<string, string>);
135
-
136
- const method = propertyMap['Method'];
137
- const resolution = propertyMap['Resolution'];
138
- const chains = propertyMap['Chains'];
139
-
140
- let chain;
141
- let positions;
142
- if (chains) {
143
- const tokens = chains.split('=');
144
- if (tokens.length === 2) {
145
- [chain, positions] = tokens;
146
- }
147
- }
148
- const output: ProcessedStructureData = {
149
- id,
150
- source: 'PDB',
151
- method,
152
- resolution: !resolution || resolution === '-' ? undefined : resolution,
153
- downloadLink: `https://www.ebi.ac.uk/pdbe/entry-files/download/pdb${id.toLowerCase()}.ent`,
154
- chain,
155
- positions,
156
- protvistaFeatureId: id,
203
+ const isCanonical = isoformMatch.sequence === canonicalSequence;
204
+ const isoformElement = isoformMatch
205
+ ? html`<a
206
+ href="${uniprotKBLink}${accession}/entry#${isoformMatch.isoformId}"
207
+ >${isoformMatch.isoformId} ${isCanonical ? '(Canonical)' : ''}</a
208
+ >`
209
+ : null;
210
+ return {
211
+ id: d.modelEntityId,
212
+ source: 'AlphaFold',
213
+ method: 'Predicted',
214
+ positions: `${d.sequenceStart}-${d.sequenceEnd}`,
215
+ protvistaFeatureId: d.modelEntityId,
216
+ downloadLink: d.pdbUrl,
217
+ amAnnotationsUrl: d.amAnnotationsUrl,
218
+ isoform: isoformElement,
157
219
  };
158
- return output;
159
220
  })
160
- .filter(
161
- (
162
- transformedItem: ProcessedStructureData | undefined
163
- ): transformedItem is ProcessedStructureData =>
164
- transformedItem !== undefined
165
- ) : [];
166
-
167
- const processAFData = (data: AlphaFoldPayload): ProcessedStructureData[] =>
168
- data.map((d) => ({
169
- id: d.modelEntityId,
170
- source: 'AlphaFold',
171
- method: 'Predicted',
172
- positions: `${d.sequenceStart}-${d.sequenceEnd}`,
173
- protvistaFeatureId: d.modelEntityId,
174
- downloadLink: d.pdbUrl,
175
- }));
176
-
177
- const process3DBeaconsData = (data: BeaconsData): ProcessedStructureData[] => {
178
- const otherStructures = data?.structures?.filter(({ summary }) =>
179
- providersFrom3DBeacons.includes(summary.provider)
180
- );
221
+ .sort((a, b) => {
222
+ return getIsoformNum(a.id) - getIsoformNum(b.id);
223
+ });
224
+
225
+ const process3DBeaconsData = (
226
+ data: BeaconsData,
227
+ accession: string | undefined,
228
+ checksum: string | undefined
229
+ ): ProcessedStructureData[] => {
230
+ // If accession is provided without checksum, filter by whitelisted providers
231
+ const filterByProviders = !!accession && !checksum;
232
+
233
+ let structures = filterByProviders
234
+ ? data?.structures?.filter(({ summary }) =>
235
+ providersFrom3DBeacons.includes(summary.provider)
236
+ )
237
+ : data?.structures?.sort(
238
+ (a, b) =>
239
+ b.summary.confidence_avg_local_score -
240
+ a.summary.confidence_avg_local_score
241
+ );
242
+
243
+ if (accession && checksum && structures) {
244
+ const matchIndex = structures.findIndex(({ summary }) =>
245
+ summary.model_identifier.includes(accession)
246
+ );
247
+
248
+ if (matchIndex !== -1) {
249
+ structures = [
250
+ structures[matchIndex],
251
+ ...structures.slice(0, matchIndex),
252
+ ...structures.slice(matchIndex + 1),
253
+ ];
254
+ }
255
+ }
256
+
181
257
  return (
182
- otherStructures?.map(({ summary }) => ({
183
- id: summary['model_identifier'],
258
+ structures?.map(({ summary }) => ({
259
+ id: summary.model_identifier,
184
260
  source: summary.provider,
185
261
  method: sourceMethods.get(summary.provider),
186
- positions: `${summary['uniprot_start']}-${summary['uniprot_end']}`,
187
- protvistaFeatureId: summary['model_identifier'],
188
- downloadLink: summary['model_url'],
189
- // isoform.io does not have a model page url. Link to their homepage instead.
262
+ positions: `${summary.uniprot_start || 1}-${
263
+ summary.uniprot_end || data.entry?.sequence.length
264
+ }`,
265
+ protvistaFeatureId: summary.model_identifier,
266
+ downloadLink: summary.model_url,
190
267
  sourceDBLink:
191
268
  summary.provider === 'isoform.io'
192
269
  ? 'https://www.isoform.io/home'
193
- : summary['model_page_url'],
270
+ : summary.model_page_url,
271
+ chain:
272
+ summary.entities?.flatMap((entity) => entity.chain_ids).join(', ') ||
273
+ undefined,
194
274
  })) || []
195
275
  );
196
276
  };
@@ -261,10 +341,12 @@ const styleId = 'protvista-styles';
261
341
  class ProtvistaUniprotStructure extends LitElement {
262
342
  accession?: string;
263
343
  sequence?: string;
344
+ checksum?: string;
264
345
  data?: ProcessedStructureData[];
265
346
  structureId?: string;
266
347
  metaInfo?: TemplateResult;
267
348
  colorTheme?: string;
349
+ isoforms?: IsoformIdSequence;
268
350
  private loading?: boolean;
269
351
  private alphamissenseAvailable?: boolean;
270
352
 
@@ -286,41 +368,77 @@ class ProtvistaUniprotStructure extends LitElement {
286
368
  return {
287
369
  accession: { type: String },
288
370
  structureId: { type: String },
371
+ checksum: { type: String },
289
372
  sequence: { type: String },
290
373
  data: { type: Object },
291
374
  loading: { type: Boolean },
292
375
  colorTheme: { type: String },
293
376
  alphamissenseAvailable: { type: Boolean },
377
+ isoforms: { type: Object, attribute: false },
294
378
  };
295
379
  }
296
380
 
297
381
  async connectedCallback() {
298
382
  super.connectedCallback();
299
- if (!this.accession) return;
383
+ if (!this.accession && !this.checksum) return;
300
384
 
301
385
  // We are showing PDBe models returned by UniProt's API as there is inconsistency between UniProt's recognised ones and 3d-beacons.
302
- const pdbUrl = `https://rest.uniprot.org/uniprotkb/${this.accession}`;
303
- const alphaFoldUrl = `https://alphafold.ebi.ac.uk/api/prediction/${this.accession}`;
304
- // exclude_provider accepts only value hence 'pdbe' as majority of the models are from there.
305
- const beaconsUrl = `https://www.ebi.ac.uk/pdbe/pdbe-kb/3dbeacons/api/uniprot/summary/${this.accession}.json?exclude_provider=pdbe`;
386
+ const pdbUrl =
387
+ this.accession && !this.checksum
388
+ ? `https://rest.uniprot.org/uniprotkb/${this.accession}`
389
+ : '';
390
+ // AlphaMissense predictions are only available in AF predictions endpoint
391
+ const alphaFoldUrl =
392
+ this.accession && !this.checksum
393
+ ? `https://alphafold.ebi.ac.uk/api/prediction/${this.accession}`
394
+ : '';
395
+ // exclude_provider accepts only value hence 'pdbe' as majority of the models are from there if querying by accession
396
+ const beaconsUrl =
397
+ this.accession && !this.checksum
398
+ ? `https://www.ebi.ac.uk/pdbe/pdbe-kb/3dbeacons/api/uniprot/summary/${this.accession}.json?exclude_provider=pdbe`
399
+ : `https://www.ebi.ac.uk/pdbe/pdbe-kb/3dbeacons/api/v2/sequence/?id=${this.checksum}&type=md5`;
306
400
 
307
401
  const rawData = await fetchAll([pdbUrl, alphaFoldUrl, beaconsUrl]);
308
402
  this.loading = false;
309
403
 
310
404
  const pdbData = processPDBData(rawData[pdbUrl] || []);
311
405
  let afData = [];
312
- // Check if AF sequence matches UniProt sequence
313
- const alphaFoldSequenceMatch = rawData[alphaFoldUrl]?.filter(
314
- ({ sequence: afSequence }) => rawData[pdbUrl]?.sequence?.value === afSequence || this.sequence === afSequence
315
- );
316
- if (alphaFoldSequenceMatch?.length) {
317
- afData = processAFData(alphaFoldSequenceMatch);
318
- this.alphamissenseAvailable = alphaFoldSequenceMatch.some(
319
- (data) => data.amAnnotationsUrl
406
+
407
+ if (this.isoforms && rawData[alphaFoldUrl]?.length) {
408
+ // Include isoforms that are provided in the UniProt isoforms mapping and ignore the rest from AF payload that are out of sync with UniProt
409
+ const alphaFoldSequenceMatches = rawData[alphaFoldUrl]?.filter(
410
+ ({ sequence: afSequence }) =>
411
+ this.isoforms?.some(({ sequence }) => afSequence === sequence)
412
+ );
413
+
414
+ afData = processAFData(
415
+ alphaFoldSequenceMatches,
416
+ this.accession,
417
+ this.isoforms,
418
+ rawData[pdbUrl]?.sequence?.value
320
419
  );
420
+
421
+ this.alphamissenseAvailable = !!afData?.[0].amAnnotationsUrl;
422
+ } else {
423
+ // Check if AF sequence matches UniProt sequence
424
+ const alphaFoldSequenceMatch = rawData[alphaFoldUrl]?.filter(
425
+ ({ sequence: afSequence }) =>
426
+ rawData[pdbUrl]?.sequence?.value === afSequence ||
427
+ this.sequence === afSequence
428
+ );
429
+ if (alphaFoldSequenceMatch?.length) {
430
+ afData = processAFData(alphaFoldSequenceMatch);
431
+ this.alphamissenseAvailable = alphaFoldSequenceMatch.some(
432
+ (data) => data.amAnnotationsUrl
433
+ );
434
+ }
321
435
  }
322
436
 
323
- const beaconsData = process3DBeaconsData(rawData[beaconsUrl] || []);
437
+ const beaconsData = process3DBeaconsData(
438
+ rawData[beaconsUrl] || [],
439
+ this.accession,
440
+ this.checksum
441
+ );
324
442
 
325
443
  // TODO: return if no data at all
326
444
  // if (!payload) return;
@@ -341,7 +459,11 @@ class ProtvistaUniprotStructure extends LitElement {
341
459
  ) as ProtvistaDatatable;
342
460
  if (!protvistaDatatableElt?.selectedid && this.data?.[0]) {
343
461
  // Select the first element in the table
344
- this.onTableRowClick({ id: this.data[0].id, source: this.data[0].source, downloadLink: this.data[0].downloadLink });
462
+ this.onTableRowClick({
463
+ id: this.data[0].id,
464
+ source: this.data[0].source,
465
+ downloadLink: this.data[0].downloadLink,
466
+ });
345
467
  protvistaDatatableElt.selectedid = this.data[0].id;
346
468
  }
347
469
  }
@@ -372,22 +494,28 @@ class ProtvistaUniprotStructure extends LitElement {
372
494
  id,
373
495
  source,
374
496
  downloadLink,
497
+ amAnnotationsUrl,
375
498
  }: {
376
499
  id: string;
377
500
  source?: string;
378
501
  downloadLink?: string;
502
+ amAnnotationsUrl?: string;
379
503
  }) {
380
- if (providersFrom3DBeacons.includes(source)) {
504
+ if (this.checksum || providersFrom3DBeacons.includes(source)) {
381
505
  this.modelUrl = downloadLink;
382
506
  // Reset the rest
383
507
  this.structureId = undefined;
384
508
  this.metaInfo = undefined;
385
509
  this.colorTheme = 'alphafold';
510
+ if (source === 'AlphaFold DB') {
511
+ this.metaInfo = AFMetaInfo;
512
+ }
386
513
  } else {
387
514
  this.structureId = id;
388
515
  this.modelUrl = undefined;
389
516
  if (this.structureId.startsWith('AF-')) {
390
517
  this.metaInfo = AFMetaInfo;
518
+ this.alphamissenseAvailable = !!amAnnotationsUrl;
391
519
  } else {
392
520
  this.metaInfo = undefined;
393
521
  }
@@ -525,6 +653,7 @@ class ProtvistaUniprotStructure extends LitElement {
525
653
  <tr>
526
654
  <th data-filter="source">Source</th>
527
655
  <th>Identifier</th>
656
+ ${this.isoforms ? html`<th>Isoform</th>` : ''}
528
657
  <th data-filter="method">Method</th>
529
658
  <th>Resolution</th>
530
659
  <th>Chain</th>
@@ -544,15 +673,23 @@ class ProtvistaUniprotStructure extends LitElement {
544
673
  positions,
545
674
  downloadLink,
546
675
  sourceDBLink,
676
+ isoform,
677
+ amAnnotationsUrl,
547
678
  }) => html`<tr
548
679
  data-id="${id}"
549
680
  @click="${() =>
550
- this.onTableRowClick({ id, source, downloadLink })}"
681
+ this.onTableRowClick({
682
+ id,
683
+ source,
684
+ downloadLink,
685
+ amAnnotationsUrl,
686
+ })}"
551
687
  >
552
688
  <td data-filter="source" data-filter-value="${source}">
553
689
  <strong>${source}</strong>
554
690
  </td>
555
691
  <td>${id}</td>
692
+ ${this.isoforms ? html`<td>${isoform}</td>` : ''}
556
693
  <td data-filter="method" data-filter-value="${method}">
557
694
  ${method}
558
695
  </td>
@@ -613,7 +750,8 @@ class ProtvistaUniprotStructure extends LitElement {
613
750
  : html``}
614
751
  ${!this.data && !this.loading
615
752
  ? html`<div class="protvista-no-results">
616
- No structure information available for ${this.accession}
753
+ No structure information available
754
+ ${this.accession ? `for ${this.accession}` : ''}
617
755
  </div>`
618
756
  : html``}
619
757
  </div>