Skip to content
Snippets Groups Projects
Commit 260597f8 authored by Ibrahim Roshan Kunnakkattu's avatar Ibrahim Roshan Kunnakkattu Committed by Sreenath Sasidharan Nair
Browse files

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

parent 329914a0
No related branches found
No related tags found
1 merge request!4Added features for aggregated ligand interaction view and interactivity with heat map
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