Skip to content
Snippets Groups Projects
Commit db7be3f5 authored by Sreenath Sasidharan Nair's avatar Sreenath Sasidharan Nair
Browse files

Merge branch 'PDBE-3362' into 'master'

Added features for aggregated ligand interaction view and interactivity with heat map

See merge request !4
parents 329914a0 260597f8
No related branches found
No related tags found
1 merge request!4Added features for aggregated ligand interaction view and interactivity with heat map
Pipeline #517434 failed with stage
in 56 seconds
Showing with 8332 additions and 1980 deletions
# PDB ligand environment component
# PDB LigandEnv component
This is a web-component to display ligand structure in 2D along with its interactions. Ligand can be perceived as a set of covalently linked pdb residues (refered to as bound molecule) or a single pdb residue. This depiction can be enriched with a substructure highlight, atom names, and binding site interactions.
This is a web-component to display ligand structure in 2D along with its interactions. This depiction can be enriched with substructure highlight, atom names, binding site interactions and aggregated protein-ligand interactions.
## Step after cloning (use local server to see demo pages)
## Installation
```shell
npm run install
npm install
npm run build
npm run start
open any of the *.html pages in the demo directory
```
To see demo, copy demo directory to build and open any of the html pages.
## Component modes
* Mode A: Display bound molecule and its interactions
* Mode B: Display ligand and its interactions
* Mode C: Display ligand (chemical component) only
* Mode A: Display ligand and its interactions (Using pdb-id, pdb-res-id and pdb-chain-id)
* Mode B: Display boundmolecule and its interactions (Using pdb-id and bound-molecule-id)
* Mode C: Display ligand only (Using pdb-res-name)
* Mode D: Display ligand and aggregated interactions (Using pdb-res-name and contact-type)
| Mode A | Mode B | Mode C |
|:------------------: | :-------: | :-------: |
| <img src="https://www.ebi.ac.uk/~lpravda/imgs/1cbs_REA_200_A.png"/>| <img src="https://www.ebi.ac.uk/~lpravda/imgs/3d12_bm1.png"/> | <img src="https://www.ebi.ac.uk/pdbe-srv/pdbechem/image/showNew?code=VIA&size=500"/> |
| [1cbs REA 200 A](https://www.ebi.ac.uk/pdbe/entry/pdb/1cbs/bound/REA) | 3D12 bm1 (`2xGLC-2xBGC-LXZ-NGA-GL0`)| [wwPDB CCD - VIA](https://pdbe.org/chem/VIA)
| Mode A | Mode B | Mode C | Mode D |
|:------------------: | :-------: | :-------: | :-------: |
| <img src="dependencies/REA_A_200.png">| <img src="dependencies/3d12_bm1.png"/> | <img src="https://ftp.ebi.ac.uk/pub/databases/msd/pdbechem_v2/ccd/V/VIA/VIA_500.svg"/> | <img src="dependencies/STI_aggregated_interactions.png"> |
| [1cbs REA 200 A](https://www.ebi.ac.uk/pdbe/entry/pdb/1cbs/bound/REA) | [3D12 bm1 (`2xGLC-2xBGC-LXZ-NGA-GL0`)](https://www.ebi.ac.uk/pdbe/entry/pdb/3d12/branched/4)| [wwPDB CCD - VIA](https://pdbe.org/chem/VIA) | STI |
## How to use it
The component can be inserted into the pages by two different ways. Either as a `web-component` using html tag, or directly by using javascript as a `plugin`.
PDB LigandEnv can be inserted into web pages in two different ways. Either as a `web-component` using html tag, or directly by using javascript as a `plugin`.
Interactions data displayed by the component can come from three different environments `Production`, `Development`, `Internal`. If no environment is specified `Production` is used as default..
......@@ -53,7 +53,7 @@ A few files needs to be imported in the page before the component is attempted t
charset="utf-8"></script>
<!--PDBe interactions component-->
<script type="module" src="pdb-ligand-env-component-0.3.0-min.js"></script>
<script type="module" src="pdb-ligand-env-component-2.0.0-min.js"></script>
```
#### A) Ligand interactions
......@@ -68,16 +68,20 @@ A few files needs to be imported in the page before the component is attempted t
<pdb-ligand-env pdb-id="3d12" bound-molecule-id="bm1"></pdb-ligand-env>
```
#### C) Ligand/chemical component
#### C) Ligand
```html
<pdb-ligand-env pdb-res-name="CLR" zoom-on ></pdb-ligand-env>
```
#### D) Ligand and aggregated interactions
```html
<pdb-ligand-env pdb-res-name="STI", contact-type=["hbond","hydrophobic","vdw"]></pdb-ligand-env>
```
The component contains a number of properties that can be set, in order to change data that are being displayed. First you need to define a component on the page:
```html
<pdb-ligand-env id='SIA-component'></pdb-ligand-env>
```
......@@ -87,14 +91,15 @@ and then inject data you want to display e.g.:
let chemUrl = `https://www.ebi.ac.uk/pdbe/static/files/pdbechem_v2/SIA/annotation`;
let interactionsURL = "https://wwwdev.ebi.ac.uk/pdbe/graph-api/pdb/bound_ligand_interactions/4yy1/A/604";
let component = document.getElementById('SIA-component');
const depiction = await (await fetch(chemUrl)).json();
const interactionsData = await (await fetch(interactionsURL)).json();
const atomsToHighlight = ['C10', 'C11', 'O10'];
component.depiction = depiction;
component.ligandHighlight = atomsToHighlight;
component.interactions = interactionsData;
(async() => {
const depiction = await (await fetch(chemUrl)).json();
const interactionsData = await (await fetch(interactionsURL)).json();
const atomsToHighlight = ['C10', 'C11', 'O10'];
component.depiction = depiction;
component.ligandHighlight = atomsToHighlight;
component.interactions = interactionsData;
})()
```
### Plugin
......@@ -148,6 +153,13 @@ this.display.initLigandInteractions('1cbs', 200, 'A');
// to display chemical component with atom names only
this.display.initLigandDisplay('HEM', true);
// to display aggregated protein-ligand interactions
this.display.initLigandDisplay('STI').then(() => {
this.display.initLigandWeights('STI').then(() => {
this.display.showWeights(["hydrophobic"]);
})
})
````
## Parameters
......@@ -157,7 +169,8 @@ this.display.initLigandDisplay('HEM', true);
| pdb-id | string | No | PDB id of a protein to retrieve interactions from. `(mode A and B only)` |
| bound-molecule-id | string | No | PDB bound molecule id `(mode A only)` |
| pdb-res-name | string | No | PDB residue name aka: *auth_comp_id* `(mode C only)`
| pdb-res-id | number | No | PDB residue id aka: *auth_seq_id* `(mode B only)`
| pdb-res-id | number | No | PDB residue id aka: *auth_seq_id* `(mode B only)` |
| contact-type | string[] | No | protein-ligand contact_type calculated by pdbe-arpeggio `(mode D only)` |
| pdb-chain-id | string | No | PDB residue chain aka: *auth_asym_id* `(mode B only)`|
| substructure | string[] | No | List of atom names to be highlighted on the ligand structure |
| color | string | No | HEX representation of the color highlight. `(Default: #D3D3D3)` |
......
<!doctype html>
<html lang="en">
<head>
<script src="https://d3js.org/d3.v5.min.js"></script>
<!-- CSS style to be used for scene drawing (required for saving SVGs.) -->
<link rel="stylesheet" href="http://127.0.0.1:8080/pdb-ligand-env-svg.css" />
<!-- UI icons -->
<link rel="stylesheet" href="https://ebi.emblstatic.net/web_guidelines/EBI-Icon-fonts/v1.3/fonts.css" />
<!-- Web component polyfill (only loads what it needs) -->
<script src="https://cdn.jsdelivr.net/npm/@webcomponents/webcomponentsjs/webcomponents-lite.js" charset="utf-8">
</script>
<script src="https://cdn.jsdelivr.net/npm/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"
charset="utf-8"></script>
<!--PDBe interactions component-->
<script type="module" src="http://127.0.0.1:8080/pdb-ligand-env-component-1.0.0-min.js"></script>
</head>
<body>
<!--Mode A-->
<div style="position: relative; float: left;">
<div style="width: 500px; height: 500px; position: relative">
<pdb-ligand-env pdb-res-name="STI", contact-type="total"></pdb-ligand-env>
</div>
</div>
</body>
</html>
\ No newline at end of file
dependencies/3d12_bm1.png

37.9 KiB

dependencies/REA_A_200.png

47.1 KiB

dependencies/STI_aggregated_interactions.png

32.8 KiB

......@@ -4,6 +4,7 @@ const del = require('del');
const concat = require('gulp-concat');
const header = require('gulp-header');
const minify = require("gulp-minify");
const cp = require('child_process');
const PACKAGE_ROOT_PATH = process.cwd();
const PKG_JSON = require(path.join(PACKAGE_ROOT_PATH, "package.json"));
......@@ -18,7 +19,7 @@ const banner = ['/**',
].join('\n');
const license = ['/**',
' * Copyright 2019-2020 Lukas Pravda <lpravda@ebi.ac.uk>',
' * Copyright 2024-2030 Protein Data Bank in Europe <pdbehelp@ebi.ac.uk>',
' * European Bioinformatics Institute (EBI, http://www.ebi.ac.uk/)',
' * European Molecular Biology Laboratory (EMBL, http://www.embl.de/)',
' * Licensed under the Apache License, Version 2.0 (the "License");',
......@@ -94,5 +95,11 @@ gulp.task('minifyPlugin', () => {
.pipe(gulp.dest('build/'));
});
gulp.task('createDoc', function (cb) {
cp.exec('./node_modules/.bin/jsdoc -c jsdoc.json', function(err, stdout, stderr){
cb(err);
})
});
gulp.task('default', gulp.series('clean', 'copyAppCSS', 'concat', 'concatCSS',
'copyXML', 'copyIndex', 'copyMapping', 'minifyPlugin'));
\ No newline at end of file
'copyXML', 'copyIndex', 'copyMapping', 'minifyPlugin', 'createDoc'));
\ No newline at end of file
{
"tags": {
"allowUnknownTags": true
},
"opts": {
"template": "node_modules/better-docs",
"destination": "build/docs"
},
"plugins": [
"node_modules/better-docs/typescript",
"plugins/markdown"
],
"source": {
"includePattern": "\\.(jsx|js|ts|tsx)$",
"include": ["README.md", "src/component", "src/plugin"]
},
"templates": {
"cleverLinks": false,
"monospaceLinks": false,
"default": {
"outputSourceFiles": true
},
"path": "ink-docstrap",
"theme": "cerulean",
"navType": "vertical",
"linenums": true,
"dateFormat": "MMMM Do YYYY, h:mm:ss a"
}
}
\ No newline at end of file
This diff is collapsed.
{
"name": "pdb-ligand-env",
"version": "1.0.0",
"version": "2.0.0",
"description": "",
"main": "app.js",
"dependencies": {
"@types/d3": "^5.16.3",
"@types/d3-tip": "^3.5.5",
"@types/ws": "^8.5.4",
"d3": "^5.16.0",
"d3-tip": "^0.9.1",
"d3scription": "^1.0.1",
"lit-element": "^2.4.0",
"typescript": "^4.0.2"
"typescript": "^5.3.3"
},
"devDependencies": {
"@babel/core": "^7.12.3",
......@@ -18,7 +19,8 @@
"@babel/preset-env": "^7.12.1",
"@babel/runtime": "^7.12.1",
"@webcomponents/webcomponentsjs": "^2.5.0",
"babel-loader": "^8.1.0",
"babel-loader": "^9.1.3",
"better-docs": "^2.7.3",
"browser-sync": "^2.26.13",
"camelcase": "^5.0.0",
"clean-webpack-plugin": "^1.0.1",
......@@ -30,14 +32,16 @@
"gulp-concat": "^2.6.1",
"gulp-header": "^2.0.9",
"gulp-minify": "^3.1.0",
"jsdoc": "^4.0.2",
"live-server": "^1.2.1",
"npm-run-all": "^4.1.3",
"onchange": "^6.1.1",
"style-loader": "^0.23.1",
"ts-node": "^7.0.1",
"url-loader": "^1.1.2",
"webpack": "^5.2.0",
"webpack-cli": "^3.3.12"
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
},
"scripts": {
"tscW": "tsc -w",
......
......@@ -3,6 +3,14 @@ import { LitElement } from 'lit-element';
import "../styles/pdb-ligand-env.css";
// Extend the LitElement base class
/**
* PDB LigandEnv component to display ligand structure in 2D along with its interactions.
* This depiction can be enriched with substructure highlight, atom names, binding site
* interactions and aggregated protein-ligand interactions
* @component
* @example <caption> Basic usage </caption>
* <pdb-ligand-env pdb-id="1cbs" pdb-res-id="200" pdb-chain-id="A" environment="development"></pdb-ligand-env>
*/
class pdbLigandEnv extends LitElement {
//Get properties / attribute values
......@@ -13,6 +21,7 @@ class pdbLigandEnv extends LitElement {
entityId: { type: String, attribute: 'entity-id' },
resName: { type: String, attribute: 'pdb-res-name' },
resId: { type: Number, attribute: 'pdb-res-id' },
contactType: {type: Array, attribute: 'contact-type', noAccessors: true},
chainId: { type: String, attribute: 'pdb-chain-id' },
substructureHighlight: { type: Array, attribute: 'substructure' },
substructureColor: { type: String, attribute: 'color' },
......@@ -22,11 +31,27 @@ class pdbLigandEnv extends LitElement {
};
}
// Create custom accessors for contactType
set contactType(value) {
let prevCType = this._contactType + "";
this._contactType = value;
if (prevCType.length > 0) {
this.renderLigandEnv();
}
}
get contactType() { return this._contactType; }
constructor() {
super();
}
async connectedCallback() {
this.renderLigandEnv();
}
renderLigandEnv() {
this.innerHTML = "";
this.highlightSubstructure = [];
let uiParams = new Config.UIParameters();
uiParams.zoom = this.zoomOn;
......@@ -47,12 +72,22 @@ class pdbLigandEnv extends LitElement {
this.display.initLigandInteractions(this.pdbId, this.resId, this.chainId);
}
}
else if (this.resName) {
this.display.initLigandDisplay(this.resName, names).then(() => this.display.centerScene());
else if (this.resName){
this.display.initLigandDisplay(this.resName, names).then(() => {
if (this.contactType){
if(this.display.ligandIntxData === undefined){
this.display.initLigandWeights(this.resName).then(() => {
this.display.showWeights(this.contactType);
})
}
else {
this.display.showWeights(this.contactType);
}
}
})
}
}
//#region properties
set depiction(data) {
if (!data) return;
......@@ -61,6 +96,10 @@ class pdbLigandEnv extends LitElement {
this.display.centerScene();
}
set atomWeights(contactType) {
this.display.showWeights(contactType);
}
set highlightSubstructure(data) {
if (!data || !this.display) {
console.log(`Argument needs to be a non empty array of strings.`);
......@@ -78,12 +117,6 @@ class pdbLigandEnv extends LitElement {
this.display.addLigandHighlight(this.substructureHighlight, this.highlightColor);
}
set contourData(data) {
if (!data || !this.display || !this.display.depiction !== undefined) return;
this.display.addContours(data);
}
set zoom(data) {
if (this.display !== undefined) this.display.toggleZoom(data);
}
......@@ -104,4 +137,4 @@ class pdbLigandEnv extends LitElement {
}
// Register the new element with the browser.
customElements.define('pdb-ligand-env', pdbLigandEnv);
\ No newline at end of file
customElements.define('pdb-ligand-env', pdbLigandEnv);
......@@ -7,10 +7,16 @@ namespace Config {
export const interactionShowLabelEvent: string = 'PDB.interactions.showLabel';
export const interactionHideLabelEvent: string = 'PDB.interactions.hideLabel';
export const LigandShowAtomEvent: string = 'PDB.ligand.showAtom';
export const LigandHideAtomEvent: string = 'PDB.ligand.hideAtom';
export const molstarClickEvent: string = 'PDB.molstar.click';
export const molstarMouseoverEvent: string = 'PDB.molstar.mouseover';
export const molstarMouseoutEvent: string = 'PDB.molstar.mouseout';
export const ligandHeatmapMouseoverEvent: string = 'PDB.ligHeatmap.mouseover';
export const ligandHeatmapMouseoutEvent: string = 'PDB.ligHeatmap.mouseout';
export const aaTypes = new Map<string, Array<string>>([
['hydrophobic', new Array<string>('A', 'I', 'L', 'M', 'F', 'W', 'V')],
['positive', Array<string>('K', 'R', 'O')],
......
/**
* This class contains all the details which are necessary for redrawing
* RDKIt style 2D molecule depiction on a client side as well as some
* other logic which should hopefully help with the initial placement of
* RDKit style 2D molecule depiction on a client side as well as some
* other logic for initial placement of
* binding partners in the residue-level view.
*
* @author Lukas Pravda <lpravda@ebi.ac.uk>
* @class Depiction
* @param {string} ccdId PDB CCD id.
* @param {Atom[]} atoms List of atoms.
* @param {Bond[]} bonds Visual representation of bonds.
* They do not correlate 1:1 with a number of bonds!
* @param {Vector2D} resolution x,y dimension of the image. Needs to be used
* for a scene shift, so it is centered.
*/
* @param {HTMLElement} parent HTMLElement on which depiction to be displayed.
* @param {any} root SVG element for the depiction.
* @param {any} data Json annotation of ligand SVG
**/
class Depiction {
ccdId: string;
atoms: Atom[];
bonds: Bond[];
resolution: Vector2D;
private parent: HTMLElement;
private root: d3.Selection<SVGGElement, unknown, null, undefined>;
private structure: d3.Selection<SVGGElement, unknown, null, undefined>;
private contour: d3.Selection<SVGGElement, unknown, null, undefined>;
private highlight: d3.Selection<SVGGElement, unknown, null, undefined>;
public weight: d3.Selection<SVGGElement, unknown, null, undefined>;
private structure: d3.Selection<SVGGElement, unknown, null, undefined>;
constructor(parent: any, data: any) {
this.root = parent
constructor(parent: HTMLElement, root: any, data: any) {
this.root = root;
this.parent = parent;
this.highlight = this.root.append('g').attr('id', 'highlight');
this.weight = this.root.append('g').attr('id', 'weight');
this.structure = this.root.append('g').attr('id', 'structure');
this.contour = this.root.append('g').attr('id', 'contour');
this.ccdId = data.ccd_id;
this.resolution = new Vector2D(data.resolution.x, data.resolution.y);
......@@ -53,7 +50,9 @@ class Depiction {
}
this.bonds.push(bond);
});
}
......@@ -62,10 +61,10 @@ class Depiction {
* atom.
*
* Present implementation sorts all the partners based on the atom
* degree and then gets the one with the lovest degree and places
* degree and then gets the one with the lowest degree and places
* the initial residue position along the vector pointing from it.
*
* @param {string[]} atomNames list of atom names the bound residue
* @param {string[]} atomNames array of atom names the bound residue
* has a contact with.
* @returns {Vector2D} Returns an initial placement of the residue in contact.
* @memberof Depiction
......@@ -89,6 +88,13 @@ class Depiction {
return new Vector2D(x, y);
}
/**
* Draws ligand structure by appending svg:path
* elements corresponding to bonds and atoms with labels
* @param {boolean} true if atom names need to be displayed
* @memberof Depiction
*/
public draw(atomNames: boolean = false) {
this.structure.selectAll("*").remove();
......@@ -99,6 +105,12 @@ class Depiction {
else this.appendLabels();
}
/**
* Highlights atoms and bonds connecting them
* @param {string[]} atoms array of atom names to higlight
* @param {string} color color to be used for higlighting
* @memberof Depiction
*/
public highlightSubgraph(atoms: Array<string>, color: string = undefined) {
if (!this.atoms || !atoms) return;
......@@ -125,17 +137,75 @@ class Depiction {
.attr('d', x => `M ${x.bgn.position.x},${x.bgn.position.y} ${x.end.position.x},${x.end.position.y}`)
.attr('style', `fill:none;fill-rule:evenodd;stroke:${color};stroke-width:22px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1`)
}
/**
* Adds circles around atoms corresponding to the value of
* their weights.The size, color and number of crcles around
* atom indicates the strength of weight. Currently maximum of
* three circles are drawn
* @param {any} weights objects with atom names and value of weight
* @param {string} colorScheme Name of color to be used for circles
* @memberof Depiction
*/
public addCircles(weights: any, colorScheme: string): void {
this.atoms.forEach(x => {
x.value = weights.filter(y => y.atom == x.name).map(z => z.value)[0]
public addContour(data: any) {
this.contour.selectAll('*').remove();
})
const data = this.atoms.filter(x => x.value >0);
const intxWeights = weights.map(x => x.value);
const gradient = new Model.Gradient(intxWeights, colorScheme).getScales();
const q1 = d3.quantile(intxWeights, 0.25);
const q3 = d3.quantile(intxWeights, 0.75);
const firstScale = gradient.firstScale;
const secondScale = gradient.secondScale;
const thirdScale = gradient.thirdScale;
this.weight.selectAll()
.data(data)
.enter()
.each(function(x: any){
if(x.value >= q1){
d3.select(this)
.append('circle')
.attr('cx', x.position.x)
.attr('cy', x.position.y)
.attr('r', secondScale.radiusScale(x.value))
.attr('fill', secondScale.colorScale(x.value))
.attr("fill-opacity", "0.5")
if(x.value >= q3){
d3.select(this)
.append('circle')
.attr('cx', x.position.x)
.attr('cy', x.position.y)
.attr('r', thirdScale.radiusScale(x.value))
.attr('fill', thirdScale.colorScale(x.value))
.attr("fill-opacity", "0.5")
}
}
});
this.contour.append('div').text(`'contour data goes here: ${data}`);
this.weight.selectAll()
.data(data)
.enter()
.append('circle')
.attr("cx", x => x.position.x)
.attr("cy", x => x.position.y)
.attr("r", x => firstScale.radiusScale(x.value))
.attr("fill", x=> firstScale.colorScale(x.value))
.attr("fill-opacity", "0.5")
.on('mouseenter', (x:Atom) => this.atomMouseEnterEventHandler(x, true))
.on('mouseleave', () => this.atomMouseLeaveEventHandler(true));
}
/**
* Appends to a given selection the visual representation of bonds as svg:path elements.
*
* Appends to a given selection the visual representation of
* bonds as svg:path elements.
* representation of the bond visuals.
* @private
* @memberof Depiction
*/
private appendBondVisuals(): void {
......@@ -149,7 +219,7 @@ class Depiction {
/**
* Append atom name labels to the visualization.
*
* @private
* @memberof Depiction
*/
private appendAtomNames() {
......@@ -167,12 +237,8 @@ class Depiction {
}
/**
* Append depiction labels to the visualization. Because RDKIt places
* the labels slightly differently this information needs to be
* consumed too, because we cannot use just atom position directly.
* Also there are all sorts of colorful subscripts and superscripts,
* so it is much easier to use it this way.
*
* Append depiction labels to the visualization.
* @private
* @memberof Depiction
*/
private appendLabels() {
......@@ -183,7 +249,7 @@ class Depiction {
.data(data)
.enter()
.append('g')
.attr('filter', 'url(#solid-background)')
.attr('filter', 'labels')
.each(function (x: any) {
for (var i = 0; i < x.labels.length; i++) {
d3.select(this)
......@@ -196,6 +262,12 @@ class Depiction {
});
}
/**
* Finds the center of an array of atoms
* @param {string} ids atom ids
* @return {Vector2D} coordinates of center
* @memeberof Depiction
*/
public getCenter(ids: string[]): Vector2D {
let coords = new Array<Vector2D>();
......@@ -214,7 +286,6 @@ class Depiction {
*
*
* @param {Map<string, number>} map
* @returns
* @memberof Depiction
*/
public sortMap(map: Map<string, number>) {
......@@ -232,9 +303,81 @@ class Depiction {
return newMap;
}
// #region event handlers
/**
* Mouse enter event handler for circles around atoms
* depicting their weights
* @public
* @param {Atom} atom object
* @param {boolean} propagation if event should be triggered on external components
* @memebrof Depiction
*/
public atomMouseEnterEventHandler(x: Atom, propagation: boolean){
this.fireExternalAtomEvent(x, propagation, Config.LigandShowAtomEvent);
}
/**
* Mouse leave event handler for circlea round atoms
* depicting their weights
* @public
* @param {boolean} propagation if event should be triggered on external components
* @memberof Depiction
*/
public atomMouseLeaveEventHandler(propagation: boolean){
this.fireExternalNullEvent(propagation, Config.LigandHideAtomEvent);
}
// #endregion
// #region fire events
/**
* Dispatches custom event to display atom names and
* corresponding weights on tooltip on mouse enter
* @private
* @param {Atom} atom object
* @param {string} eventName name of event
* @memeberof Depiction
*/
private fireExternalAtomEvent(atom: Atom, propagation:boolean, eventName: string){
const e = new CustomEvent(eventName, {
bubbles: true,
detail: {
tooltip: atom.toTooltip(),
external: propagation
}
});
this.parent.dispatchEvent(e);
}
/**
* Dispatches event to hide tooltip on mouse leave
* @private
* @param {boolean} propagation if event should be triggered on external components
* @param {string} eventName name of event
* @memeberof Depiction
*/
private fireExternalNullEvent(propagation:boolean, eventName: string) {
const e = new CustomEvent(eventName, {
bubbles: true,
detail: {
external: propagation
}
});
this.parent.dispatchEvent(e);
}
// #endregion
}
/**
* Atom from the depiction
*
......@@ -247,27 +390,37 @@ class Atom {
name: string;
labels: any;
position: Vector2D;
connectivity: number
connectivity: number;
value: number;
constructor(item: any) {
this.name = item.name;
this.labels = item.labels;
this.position = new Vector2D(item.x, item.y);
this.connectivity = 0;
this.value = 0;
}
/**
*
*
* @param {Atom} other
* @returns true if the atoms are equal
* @returns {boolean} true if the atoms are equal
* @memberof Atom
*/
public equals(other: Atom) {
public equals(other: Atom): boolean {
if (!(other instanceof Atom)) return false;
return other.name === this.name;
}
/**
* @return {string} name of atom
* @memberof Atom
*/
public toTooltip(): string {
return `<span>${this.name} | ${this.value}</span>`;
}
}
/**
......@@ -340,7 +493,6 @@ class Vector2D {
}
}
/**
* Represents a bond in a 2D depiction.
*
......@@ -356,12 +508,6 @@ class Bond {
coords: string;
style: string;
/**
*Creates an instance of the bond.
* @param {Atom} a
* @param {Atom} b
* @memberof Bond
*/
constructor(a: Atom, b: Atom, coords: string, style: string) {
this.bgn = a;
this.end = b;
......@@ -389,7 +535,7 @@ class Bond {
* Check whether or not a bond contains the atom.
*
* @param {Atom} other The other side of the bond
* @returns True if the atom is a part of the bond, false otherwise.
* @returns {boolean} True if the atom is a part of the bond, false otherwise.
* @memberof Bond
*/
public containsAtom(other: Atom) {
......
/**
* This class contains methods for creating all the visualization
* components of the LigandEnv
*
* @class Visualization
* @param {HTMLElement} element HTMLElement to display the visualization
* @param {Config.UIParameters} uiParameters UI parameter configurations
* @param {string} env environment to fetch data from
*
*/
class Visualization {
// component related
private parent: HTMLElement;
......@@ -26,6 +36,7 @@ class Visualization {
private visualsMapper: VisualsMapper;
private interactionsData: any;
private ligandIntxData: any;
private selectedResidueHash: string;
private nodeDragged: boolean;
......@@ -68,8 +79,11 @@ class Visualization {
if (uiParameters.zoom) this.zoomHandler = this.getZoomHandler();
document.addEventListener(Config.ligandHeatmapMouseoverEvent, e => this.ligHeatmapMouseoverEventHandler(e));
document.addEventListener(Config.ligandHeatmapMouseoutEvent, () => this.ligHeatmapMouseoutEventHandler());
document.addEventListener(Config.molstarClickEvent, e => this.molstarClickEventHandler(e));
document.addEventListener(Config.molstarMouseoverEvent, e => this.molstarClickEventHandler(e));
document.addEventListener(Config.molstarMouseoverEvent, e => this.molstarClickEventHandler(e));
document.addEventListener(Config.molstarMouseoutEvent, () => this.molstarMouseoutEventHandler());
d3.select(this.parent).on('resize', () => this.resize());
......@@ -95,7 +109,7 @@ class Visualization {
private molstarClickEventHandler(e: any) {
if (this.fullScreen) return;
let hash = `${e.eventData.auth_asym_id}${e.eventData.auth_seq_id}${e.eventData.ins_code}`;
let hash = `${e.eventData.auth_asym_id}${e.eventData.auth_seq_id}${e.eventData.ins_code}`;
this.nodes?.each((node: Model.InteractionNode, index: number, group: any) => {
this.nodeDim(node, index, group);
......@@ -109,6 +123,22 @@ class Visualization {
}
private ligHeatmapMouseoverEventHandler(e: any) {
if (this.depiction === undefined) return;
const atomName = e.detail.name; // CustomEvent
const atom = this.depiction.atoms.filter(x => x.value >0 && x.name === atomName);
if (atom.length > 0) {
this.depiction.atomMouseEnterEventHandler(atom[0], true);
} else {
this.depiction.atomMouseLeaveEventHandler(false);
}
}
private ligHeatmapMouseoutEventHandler() {
if (this.depiction === undefined) return;
this.depiction.atomMouseLeaveEventHandler(true);
}
/**
* Handles mouse leave molstar event. Removes interaction node highlight
......@@ -260,7 +290,7 @@ class Visualization {
* PDBeChem process.
*
* @param {string} ligandId
* @returns
* @param {boolean} withNames true for displaying atom names
* @memberof Visualization
*/
public async initLigandDisplay(ligandId: string, withNames: boolean = false) {
......@@ -272,19 +302,50 @@ class Visualization {
.then(() => this.centerScene());
}
/**
* Download aggregated protein-ligand interactions data.
*
* @param {string} ligandId
* @memberof Visualization
*/
public async initLigandWeights(ligandId: string){
const weightUrl = `https://raw.githubusercontent.com/roshkjr/Learning-sources/main/${ligandId}_atom_residue_intx.json`
return d3.json(weightUrl)
.then((d: any) => this.ligandIntxData = d);
}
/**
* Add depiction to the canvas from external resource.
*
* @param {*} depiction Content of annotation.json file generated by
* @param {any} depiction Content of annotation.json file generated by
* the PDBeChem process.
* @param {boolean} withNames true for displaying atom names
* @memberof Visualization
*/
public addDepiction(depiction: any, withNames: boolean) {
this.depiction = new Depiction(this.depictionRoot, depiction);
this.depiction = new Depiction(this.parent, this.depictionRoot, depiction);
this.depiction.draw(withNames);
}
/**
* Adds circles around atoms higlighting the weights of atoms.
*
* @param {string} contactType
* @memberof Visualization
*/
public showWeights(contactType: string[]){
if ((this.depiction === undefined) || (this.ligandIntxData) === undefined) return;
const atomPropensity= new Model.LigandIntx(this.ligandIntxData, contactType).getAtomIntxPropensity();
const colorScheme = "Greens";
this.depiction.addCircles(atomPropensity, colorScheme);
if(this.zoomHandler !== undefined){
this.zoomHandler(this.svg, d3.zoomIdentity)
};
}
/**
* Show depiction with/without atom names
......@@ -312,26 +373,16 @@ class Visualization {
* @memberof Visualization
*/
public addLigandHighlight(highlight: string[], color: string = undefined) {
if (!this.depiction) return;
this.depiction.highlightSubgraph(highlight, color);
}
/**
* Add contours to the ligand structure. The previous contours are
* going to be removed.
*
* @param {*} data
* @memberof Visualization
*/
public addContours(data: any) {
this.depiction.addContour(data);
}
/**
* Add ligand interactions to the canvas
*
* @param {*} data Data content of the API end point
* /pdb/bound_ligand_interactions
* @param {boolean} true if atom names to be displayed
* @memberof Visualization
*/
public addLigandInteractions(data: any, withNames: boolean = false) {
......@@ -356,7 +407,7 @@ class Visualization {
/**
* Add bound molecule interactions to the canvas.
*
* @param {*} data Data content of the API end point
* @param {any} data Data content of the API end point
* /pdb/bound_molecule_interactions
* @param {string} bmId Bound molecule id
* @memberof Visualization
......@@ -376,6 +427,7 @@ class Visualization {
// #region menu functions
/**
* Export scene into an SVG components. It relies on the availability
* of the external CSS for SVG styling. Otherwise it does not work.
......@@ -611,6 +663,7 @@ class Visualization {
this.parent.dispatchEvent(e);
}
// #endregion fire events
......@@ -668,7 +721,6 @@ class Visualization {
* @param {Model.InteractionNode} n Interaction node user clicked to
* @param {number} i index o the interaction node
* @param {*} g group of interaction nodes
* @returns
* @memberof Visualization
*/
private selectLigand(n: Model.InteractionNode, i: number, g: any) {
......@@ -783,7 +835,7 @@ class Visualization {
/**
* Setup display of interactions for bound molecule.
* * This includes: setup of links, nodes, simulation and subscribing to relevant events.
* This includes: setup of links, nodes, simulation and subscribing to relevant events.
* No depiction is required for this step.
*
* @private
......
......@@ -59,6 +59,8 @@ namespace Model {
/**
* Data model representing a single residue, which is a part of the
* bound molecule. This residue is unique in the entire binding site.
* @param {any} data
* @param {boolean} isLigand true if the residue is a ligand
*/
export class Residue {
id: string
......@@ -496,4 +498,114 @@ namespace Model {
}
}
export class LigandIntx{
data: any;
contactTypes: string[];
filteredData: any;
constructor(data: any, contactTypes:string[]){
this.data = data;
this.contactTypes = contactTypes;
this.filteredData = this.getFilteredData();
}
private getAtomResidueObj(x: string[]){
return {
"atom": x[0],
"AA": x[1],
"count": x[2]
}
}
private getFilteredData(){
const filteredData = new Array();
for (const contactType in this.data){
if (this.contactTypes.includes(contactType)){
filteredData.push(...this.data[contactType].map(this.getAtomResidueObj));
}
}
return filteredData
}
public getAtomIntxPropensity(){
const atomCount = new Array();
this.filteredData.reduce(function(acc, currentValue){
if(!acc[currentValue.atom]){
acc[currentValue.atom] = {"atom": currentValue.atom, "count": 0};
atomCount.push(acc[currentValue.atom]);
}
acc[currentValue.atom].count += currentValue.count;
return acc
},{});
const totalIntx = atomCount.reduce(function(acc, currentValue){
acc += currentValue.count;
return acc;
}, 0)
const atomPropensity = atomCount.map((x) =>
({"atom": x.atom,
"value": (x.count/totalIntx).toFixed(2)
})
);
return atomPropensity
}
}
/**
* Data model representing scale of a circles
* Stores scale for radius and color
*/
export class Scale{
radiusScale: any;
colorScale: any;
constructor(radiusScale:any, colorScale:any){
this.radiusScale = radiusScale;
this.colorScale = colorScale;
}
}
/**
* Gradient object with three
* level scales for circles to highlight
* @param {any} weight
* @param {string} colorScheme color to be used for generating gradient
*/
export class Gradient {
weight: any;
colorScheme: string;
constructor(weight: any, colorScheme:string){
this.weight = weight;
this.colorScheme = colorScheme;
}
/**
* Generates three level scales for the
* Gradient object
*/
public getScales(){
const color = d3[`scheme${this.colorScheme}`][9];
const weightMax = Number(d3.max(this.weight));
const gradient = {firstScale: new Scale(d3.scaleLinear([0, weightMax], [20, 30]),
d3.scaleLinear([0,weightMax], ["#FFFFFF", color[4]])),
secondScale: new Scale(d3.scaleLinear([0,weightMax], [10, 20]),
d3.scaleLinear([0, weightMax], [color[5], color[7]])),
thirdScale: new Scale(d3.scaleLinear([0, weightMax], [2, 10]),
d3.scaleLinear([0, weightMax], [color[8], color[9]]))
};
return gradient
}
}
}
\ No newline at end of file
......@@ -45,7 +45,7 @@ class ResidueProvider {
* Get single letter abbreviation of the ligand.
*
* @param {string} name Name of the chemical compound
* @returns Single letter abbreviation of the ligand
* @returns {string} Single letter abbreviation of the ligand
* @memberof ResidueProvider
*/
public getAminoAcidAbbreviation(name: string): string {
......@@ -57,7 +57,6 @@ class ResidueProvider {
* Fetch single letter abbreviations from API if they are not present
*
* @param {string} n
* @returns
* @memberof ResidueProvider
*/
public downloadAnnotation(r: Model.Residue): void {
......
......@@ -128,7 +128,6 @@ namespace Resources {
* @param {string} chainId
* @param {number} resId
* @param {Model.Environment} env
* @returns
*/
export function ligandInteractionsAPI(pdbId: string, chainId: string, resId: number, env: Model.Environment) {
let url = '';
......
......@@ -114,6 +114,9 @@ let helpBonds = `
// #endregion help
/**
* @class UI
*/
class UI {
......@@ -143,7 +146,6 @@ class UI {
*
* @param {Config.UIParameters} p Object with annotation which
* UI elements should be created.
* @returns
* @memberof UI
*/
public register(p: Config.UIParameters) {
......@@ -263,6 +265,8 @@ class UI {
this.parent.addEventListener(Config.interactionClickEvent, e => this.nodeMouseEnterEventHandler(e));
this.parent.addEventListener(Config.interactionMouseoverEvent, e => this.nodeMouseEnterEventHandler(e));
this.parent.addEventListener(Config.interactionMouseoutEvent, () => this.nodeMouseLeaveEventHandler());
this.parent.addEventListener(Config.LigandShowAtomEvent, e => this.nodeMouseEnterEventHandler(e));
this.parent.addEventListener(Config.LigandHideAtomEvent, () => this.nodeMouseLeaveEventHandler());
}
if (this.residueLabel !== undefined) {
......
......@@ -10,7 +10,7 @@
"declaration": true,
"module": "none",
"moduleResolution": "node",
"out": "build/pdb-ligand-env-plugin.js"
"outFile": "build/pdb-ligand-env-plugin.js"
},
"include": ["src/plugin/**/*", ],
"exclude": [
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment