Skip to content

Commit d5949ac

Browse files
committed
Interactions extension - process API data
1 parent 9a45201 commit d5949ac

File tree

4 files changed

+190
-45
lines changed

4 files changed

+190
-45
lines changed

index.html

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,6 @@ <h2>PDBe Molstar</h2>
7878

7979
<div class="content">
8080
<div class="controlsSection">
81-
<h3>Tmp - interaction extension</h3>
82-
<div class="controlBox">
83-
<h4>Interaction extension</h4>
84-
<button onclick="PDBeMolstarPlugin.extensions.Interactions.foo(viewerInstance)">Magic!</button>
85-
<!-- <button onclick="PDBeMolstarPlugin.extensions.Interactions.foo(viewerInstance).then(ints => setTimeout(() => ints.delete(), 1000))">Tmp magic!</button> -->
86-
</div>
87-
8881
<h3>Canvas/ layout methods</h3>
8982
<div class="controlBox">
9083
<h4>Set Background</h4>
@@ -213,6 +206,16 @@ <h4>Update data</h4>
213206
</button>
214207
</div>
215208

209+
<h3>Extensions</h3>
210+
<div class="controlBox">
211+
<h4>Interaction extension</h4>
212+
<button onclick="PDBeMolstarPlugin.extensions.Interactions.loadInteractions_example(viewerInstance)">Show interactions (1hda)</button>
213+
<!-- <button onclick="PDBeMolstarPlugin.extensions.Interactions.loadInteractionsFromApi(viewerInstance, { pdbId: '1cbs', authAsymId: 'A', authSeqId: 200 })">Show interactions from API (1cbs)</button>
214+
<button onclick="PDBeMolstarPlugin.extensions.Interactions.loadInteractionsFromApi(viewerInstance, { pdbId: '1tqn', authAsymId: 'A', authSeqId: 508 })">Show interactions from API (1tqn)</button> -->
215+
<button onclick="PDBeMolstarPlugin.extensions.Interactions.loadInteractionsFromApi(viewerInstance, { pdbId: '1hda', authAsymId: 'C', authSeqId: 143 })">Show interactions from API (1hda)</button>
216+
<button onclick="PDBeMolstarPlugin.extensions.Interactions.clearInteractions(viewerInstance)">Clear</button>
217+
</div>
218+
216219
<div>
217220
<h3>Options</h3>
218221
<div class="controlBox">
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { ColorT } from 'molstar/lib/extensions/mvs/tree/mvs/param-types';
2+
import { Interaction } from './index';
3+
4+
5+
/** Data type server by API https://www.ebi.ac.uk/pdbe/graph-api/pdb/bound_ligand_interactions/1tqn/A/508 */
6+
interface InteractionsApiData {
7+
[pdbId: string]: {
8+
interactions: {
9+
end: {
10+
/** e.g. 'CZ' */
11+
atom_names: string[],
12+
author_insertion_code?: string,
13+
author_residue_number: number,
14+
/** auth_asym_id */
15+
chain_id: string,
16+
chem_comp_id: string,
17+
},
18+
distance: number,
19+
/** e.g. 'AMIDERING', 'CARBONPI', 'DONORPI', 'carbonyl', 'covalent', 'hbond', 'hydrophobic', 'metal_complex', 'polar', 'vdw', 'vdw_clash', 'weak_hbond', 'weak_polar'... */
20+
interaction_details: string[],
21+
/** e.g.'atom-atom' */
22+
interaction_type: string,
23+
/** e.g. 'C11' */
24+
ligand_atoms: string[],
25+
}[],
26+
ligand: {
27+
author_insertion_code?: string,
28+
author_residue_number: number,
29+
/** auth_asym_id */
30+
chain_id: string,
31+
chem_comp_id: string,
32+
},
33+
}[],
34+
}
35+
36+
const InteractionTypeColors: Record<string, ColorT> = {
37+
'AMIDERING': 'red',
38+
'CARBONPI': 'magenta',
39+
'DONORPI': 'magenta',
40+
'carbonyl': '#ffffff',
41+
'covalent': '#ffffff',
42+
'hbond': '#00ffff',
43+
'hydrophobic': 'yellow',
44+
'metal_complex': '#00ff00',
45+
'polar': '#0000ff',
46+
'vdw': '#ffffff',
47+
'vdw_clash': 'red',
48+
'weak_hbond': '#00aaaa',
49+
'weak_polar': '#0000aa',
50+
51+
'_DEFAULT_': 'gray',
52+
'_MIXED_': 'gray',
53+
// TODO collect all possible values and decide on colors, this is non-exhaustive list with random colors
54+
} as const;
55+
56+
57+
export async function getInteractionApiData(params: { pdbId: string, authAsymId: string, authSeqId: number, pdbeBaseUrl: string }): Promise<InteractionsApiData> {
58+
const pdbeBaseUrl = params.pdbeBaseUrl.replace(/\/$/, '');
59+
const url = `${pdbeBaseUrl}/graph-api/pdb/bound_ligand_interactions/${params.pdbId}/${params.authAsymId}/${params.authSeqId}`;
60+
const response = await fetch(url);
61+
if (response.status === 404) return {};
62+
if (!response.ok) throw new Error(`Failed to fetch atom interaction data from ${url}`);
63+
return await response.json();
64+
}
65+
66+
export function interactionsFromApiData(data: InteractionsApiData, pdbId: string): Interaction[] {
67+
const out: Interaction[] = [];
68+
for (const { interactions, ligand } of data[pdbId] ?? []) {
69+
for (const int of interactions) {
70+
const details = int.interaction_details;
71+
const color = details.length === 1 ? (InteractionTypeColors[details[0]] ?? InteractionTypeColors._DEFAULT_) : InteractionTypeColors._MIXED_;
72+
const tooltipHeader = details.length === 1 ? `<strong>${formatInteractionType(details[0])} interaction</strong>` : `<strong>Mixed interaction</strong><br>${details.map(formatInteractionType).join(', ')}`;
73+
const tooltipPartner1 = `${ligand.chem_comp_id} ${ligand.author_residue_number}${ligand.author_insertion_code?.trim() ?? ''} | ${int.ligand_atoms.join(', ')}`;
74+
const tooltipPartner2 = `${int.end.chem_comp_id} ${int.end.author_residue_number}${int.end.author_insertion_code?.trim() ?? ''} | ${int.end.atom_names.join(', ')}`;
75+
const tooltip = `${tooltipHeader}<br>${tooltipPartner1}${tooltipPartner2}`;
76+
77+
out.push({
78+
start: {
79+
auth_asym_id: ligand.chain_id,
80+
auth_seq_id: ligand.author_residue_number,
81+
auth_ins_code_id: normalizeInsertionCode(ligand.author_insertion_code),
82+
atoms: int.ligand_atoms,
83+
},
84+
end: {
85+
auth_asym_id: int.end.chain_id,
86+
auth_seq_id: int.end.author_residue_number,
87+
auth_ins_code_id: normalizeInsertionCode(int.end.author_insertion_code),
88+
atoms: int.end.atom_names,
89+
},
90+
color,
91+
tooltip,
92+
});
93+
}
94+
}
95+
return out;
96+
}
97+
98+
/** Deal with special cases where ' ' or '' means undefined */
99+
function normalizeInsertionCode(insCode: string | undefined): string | undefined {
100+
if (insCode?.trim()) return insCode;
101+
else return undefined;
102+
}
103+
104+
function formatInteractionType(type: string): string {
105+
// TODO present interaction types in a nicer way ('DONORPI' or 'Vdw clash' looks ugly)
106+
return type.replace('_', ' ').replace(/^\w/g, c => c.toUpperCase());
107+
}

src/app/extensions/interactions/index.ts

Lines changed: 71 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,16 @@ import { MVSData } from 'molstar/lib/extensions/mvs/mvs-data';
55
import { MolstarSubtree } from 'molstar/lib/extensions/mvs/tree/molstar/molstar-tree';
66
import { ColorT } from 'molstar/lib/extensions/mvs/tree/mvs/param-types';
77
import { ShapeRepresentation3D } from 'molstar/lib/mol-plugin-state/transforms/representation';
8-
import { AnyColor } from 'src/app/spec';
98
import { PDBeMolstarPlugin } from '../..';
109
import { QueryParam, queryParamsToMvsComponentExpressions } from '../../helpers';
10+
import { ExtensionCustomState } from '../../plugin-custom-state';
11+
import { AnyColor } from '../../spec';
12+
import { getInteractionApiData, interactionsFromApiData } from './api';
13+
14+
15+
/** Name used when registering extension, custom state, etc. */
16+
const InteractionsExtensionName = 'pdbe-custom-interactions';
17+
const getExtensionCustomState = ExtensionCustomState.getter<{ visuals: StateObjectHandle[] }>(InteractionsExtensionName);
1118

1219

1320
export interface Interaction {
@@ -17,46 +24,55 @@ export interface Interaction {
1724
tooltip?: string,
1825
}
1926

20-
const dummyData: Interaction[] = [
21-
{
22-
start: { auth_asym_id: 'A', auth_seq_id: 45, atoms: ['CA'] },
23-
end: { auth_asym_id: 'A', auth_seq_id: 50, atoms: ['CA'] },
24-
color: 'yellow',
25-
tooltip: '<b>Hydrophobic interaction</b><br/> GLN 45 | CA — PHE 50 | CA',
26-
},
27-
{
28-
start: { auth_asym_id: 'A', auth_seq_id: 50, atoms: ['CA'] },
29-
end: { auth_asym_id: 'A', auth_seq_id: 65, atoms: ['CA'] },
30-
color: 'red',
31-
tooltip: '<b>Ion interaction</b><br/> PHE 50 | CA — PHE 65 | CA',
32-
},
33-
];
27+
export interface StateObjectHandle {
28+
/** State transform reference */
29+
ref: string,
30+
/** Remove state object from state hierarchy */
31+
delete: () => Promise<void>,
32+
}
33+
34+
export function loadInteractions_example(viewer: PDBeMolstarPlugin) {
35+
return loadInteractions(viewer, { interactions: exampleData });
36+
}
3437

35-
export function foo(viewer: PDBeMolstarPlugin) {
36-
return loadInteractions(viewer, { interactions: dummyData });
38+
export async function loadInteractionsFromApi(viewer: PDBeMolstarPlugin, params: { pdbId: string, authAsymId: string, authSeqId: number, structureId?: string }) {
39+
const data = await getInteractionApiData({ ...params, pdbeBaseUrl: viewer.initParams.pdbeUrl });
40+
const interactions = interactionsFromApiData(data, params.pdbId);
41+
await loadInteractions(viewer, { interactions, structureId: params.structureId });
3742
}
3843

3944
/** Show custom atom interactions */
40-
export async function loadInteractions(viewer: PDBeMolstarPlugin, params: { interactions: Interaction[], structureId?: string }) {
45+
export async function loadInteractions(viewer: PDBeMolstarPlugin, params: { interactions: Interaction[], structureId?: string }): Promise<StateObjectHandle> {
4146
const structureId = params.structureId ?? PDBeMolstarPlugin.MAIN_STRUCTURE_ID;
4247
const struct = viewer.getStructure(structureId);
4348
if (!struct) throw new Error(`Did not find structure with ID "${structureId}"`);
4449

4550
const primitivesMvsNode = interactionsToMvsPrimitiveData(params.interactions);
4651

4752
const update = viewer.plugin.build();
48-
const data = update.to(struct.cell).apply(MVSInlinePrimitiveData, { node: primitivesMvsNode as any }); // TODO tags
49-
data.apply(MVSBuildPrimitiveShape, { kind: 'mesh' }).apply(ShapeRepresentation3D);
50-
data.apply(MVSBuildPrimitiveShape, { kind: 'lines' }).apply(ShapeRepresentation3D);
51-
data.apply(MVSBuildPrimitiveShape, { kind: 'labels' }).apply(ShapeRepresentation3D); // TODO tags
53+
const data = update.to(struct.cell).apply(MVSInlinePrimitiveData, { node: primitivesMvsNode as any }, { tags: ['custom-interactions-data'] });
54+
data.apply(MVSBuildPrimitiveShape, { kind: 'mesh' }).apply(ShapeRepresentation3D, {}, { tags: ['custom-interactions-mesh'] });
55+
data.apply(MVSBuildPrimitiveShape, { kind: 'lines' }).apply(ShapeRepresentation3D, {}, { tags: ['custom-interactions-lines'] });
56+
data.apply(MVSBuildPrimitiveShape, { kind: 'labels' }).apply(ShapeRepresentation3D, {}, { tags: ['custom-interactions-labels'] });
5257
await update.commit();
5358

54-
return {
59+
const visual: StateObjectHandle = {
5560
ref: data.ref,
56-
delete(): Promise<void> {
57-
return viewer.plugin.build().delete(data.ref).commit();
58-
},
61+
delete: () => viewer.plugin.build().delete(data.ref).commit(),
5962
};
63+
const visualsList = getExtensionCustomState(viewer.plugin).visuals ??= [];
64+
visualsList.push(visual);
65+
return visual;
66+
}
67+
68+
/** Remove any previously added interactions */
69+
export async function clearInteractions(viewer: PDBeMolstarPlugin): Promise<void> {
70+
const visuals = getExtensionCustomState(viewer.plugin).visuals;
71+
if (!visuals) return;
72+
for (const visual of visuals) {
73+
await visual.delete();
74+
}
75+
visuals.length = 0;
6076
}
6177

6278
function interactionsToMvsPrimitiveData(interactions: Interaction[]): MolstarSubtree<'primitives'> {
@@ -67,25 +83,42 @@ function interactionsToMvsPrimitiveData(interactions: Interaction[]): MolstarSub
6783
primitives.tube({
6884
start: { expressions: queryParamsToMvsComponentExpressions([interaction.start]) },
6985
end: { expressions: queryParamsToMvsComponentExpressions([interaction.end]) },
70-
radius: 0.1,
86+
radius: 0.075,
7187
dash_length: 0.1,
7288
color: interaction.color as ColorT,
7389
tooltip: interaction.tooltip,
7490
});
7591
}
76-
// use primitives.distance to add labels to tubes
77-
// primitives.distance({
78-
// start: { auth_asym_id: 'A', auth_seq_id: 50, auth_atom_id: 'CA' },
79-
// end: { auth_asym_id: 'A', auth_seq_id: 65, auth_atom_id: 'CA' },
80-
// radius: 0.1,
81-
// dash_length: 0.1,
82-
// color: 'yellow',
83-
// label_template: 'hydrophobic',
84-
// label_color: 'yellow',
85-
// label_size: 0.5,
86-
// });
8792
const state = builder.getState();
8893
const primitivesNode = state.root.children?.find(child => child.kind === 'primitives') as MolstarSubtree<'primitives'> | undefined;
8994
if (!primitivesNode) throw new Error('AssertionError: Failed to create MVS "primitives" subtree.');
9095
return primitivesNode;
9196
}
97+
98+
/** Selected interactions from https://www.ebi.ac.uk/pdbe/graph-api/pdb/bound_ligand_interactions/1hda/C/143 */
99+
const exampleData = [
100+
{
101+
'start': { 'auth_asym_id': 'C', 'auth_seq_id': 143, 'atoms': ['CBC'] },
102+
'end': { 'auth_asym_id': 'C', 'auth_seq_id': 32, 'atoms': ['CE'] },
103+
'color': 'yellow',
104+
'tooltip': '<strong>Hydrophobic interaction</strong><br>HEM 143 | CBC — MET 32 | CE',
105+
},
106+
{
107+
'start': { 'auth_asym_id': 'C', 'auth_seq_id': 143, 'atoms': ['CBC'] },
108+
'end': { 'auth_asym_id': 'C', 'auth_seq_id': 32, 'atoms': ['SD'] },
109+
'color': 'yellow',
110+
'tooltip': '<strong>Hydrophobic interaction</strong><br>HEM 143 | CBC — MET 32 | SD',
111+
},
112+
{
113+
'start': { 'auth_asym_id': 'C', 'auth_seq_id': 143, 'atoms': ['CMD'] },
114+
'end': { 'auth_asym_id': 'C', 'auth_seq_id': 42, 'atoms': ['O'] },
115+
'color': 'gray',
116+
'tooltip': '<strong>Mixed interaction</strong><br>Vdw, Weak polar<br>HEM 143 | CMD — TYR 42 | O',
117+
},
118+
{
119+
'start': { 'auth_asym_id': 'C', 'auth_seq_id': 143, 'atoms': ['C1B', 'C2B', 'C3B', 'C4B', 'NB'] },
120+
'end': { 'auth_asym_id': 'C', 'auth_seq_id': 136, 'atoms': ['CD1'] },
121+
'color': 'magenta',
122+
'tooltip': '<strong>CARBONPI interaction</strong><br>HEM 143 | C1B, C2B, C3B, C4B, NB — LEU 136 | CD1',
123+
},
124+
];

src/app/helpers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@ export function queryParamsToMvsComponentExpressions(params: QueryParam[]): Comp
241241
type_symbol: undefined,
242242
atom_id: item.atom_id,
243243
atom_index: undefined,
244+
label_comp_id: item.label_comp_id,
245+
auth_comp_id: undefined,
244246
}));
245247
}
246248

0 commit comments

Comments
 (0)