Skip to content
Snippets Groups Projects
Commit e0b0db4e authored by Lukas Pravda's avatar Lukas Pravda
Browse files

Improve residue-level view

Binding partners are placed along the vector of one of the bonds from the atom they are bound to.
parent 7e5cc4f3
No related branches found
No related tags found
No related merge requests found
Pipeline #12258 passed with stages
in 15 seconds
# RELEASE 0.1 - 28 January 2019
First release for public testing.
## Features
* Bound molecule interactions view
* Residue-level interactions view
* Schematic view of glycans ([SNFG](https://www.ncbi.nlm.nih.gov/glycans/snfg.html)).
* Download interactions in `JSON` format.
* Save image in `SVG` format.
* Basic help and controls.
......@@ -111,9 +111,10 @@ declare class Depiction {
labels: any;
constructor();
load(id: string): Promise<this>;
getInitalAtomPosition(atomName: string): number[];
getInitalAtomPosition(atomNames: string[]): number[];
appendBondVisualsTo(root: any): void;
appendTextsTo(root: any): void;
sortMap(map: Map<string, number>): Map<string, number>;
}
declare class Atom {
name: string;
......
......@@ -29,10 +29,10 @@ var Models;
if (['SER', 'SEP', 'THR', 'ASN', 'GLN', 'TPO'].includes(this.label_comp_id)) {
return 'polar';
}
if (['ALA', 'VAL', 'ILE', 'LEU', 'MET', 'MSE'].includes(this.label_comp_id)) {
if (['ALA', 'VAL', 'ILE', 'LEU', 'MET', 'PRO', 'MSE'].includes(this.label_comp_id)) {
return 'aliphatic';
}
if (['TRP', 'TYR', 'PHE', 'HIS', 'PRO'].includes(this.label_comp_id)) {
if (['TRP', 'TYR', 'PHE', 'HIS'].includes(this.label_comp_id)) {
return 'aromatic';
}
return 'others';
......@@ -338,10 +338,23 @@ class Depiction {
return this;
});
}
getInitalAtomPosition(atomName) {
let thisAtom = this.atoms.find(x => x.name === atomName);
getInitalAtomPosition(atomNames) {
if (this.atoms.length === 1) {
return [this.atoms[0].x, this.atoms[0].y];
}
// ideally we want to find an atom which is part just a single bond to get nice initial position.
// If there is no such atom any will do
let searchStruct = new Map();
this.bonds.forEach(x => {
searchStruct.set(x.bgn.name, searchStruct.get(x.bgn.name) === undefined ? 1 : searchStruct.get(x.bgn.name) + 1);
searchStruct.set(x.end.name, searchStruct.get(x.end.name) === undefined ? 1 : searchStruct.get(x.end.name) + 1);
});
searchStruct = this.sortMap(searchStruct); // ascending order so we hit the those with less partners sooner.
let thisAtomName = [...searchStruct.keys()].find(x => atomNames.findIndex(y => y === x) !== -1);
let thisAtom = this.atoms.find(x => x.name === thisAtomName);
let bond = this.bonds.find(x => x.containsAtom(thisAtom));
let otherAtom = bond.getOtherAtom(thisAtom);
// to place the residue node a bond apart from the bondin atom
let v = [otherAtom.x - (2 * (otherAtom.x - thisAtom.x)), otherAtom.y - 2 * ((otherAtom.y - thisAtom.y))];
return v;
}
......@@ -370,6 +383,19 @@ class Depiction {
}
});
}
sortMap(map) {
let vals = [...map.values()].sort();
let newMap = new Map();
vals.forEach(x => {
map.forEach((value, key) => {
if (x === value) {
newMap.set(key, x);
return;
}
});
});
return newMap;
}
}
class Atom {
constructor(item) {
......@@ -588,8 +614,8 @@ class Visualization {
visuals.nodes.forEach(x => {
if (!(x instanceof Models.ResidueNode))
return;
let lnk = visuals.links.find(y => y.source.equals(x));
let position = depiction.getInitalAtomPosition(lnk.target.atomName);
let lnks = visuals.links.filter(y => y.source.equals(x)).map(x => x.target.atomName);
let position = depiction.getInitalAtomPosition(lnks);
x.x = position[0] + Math.random() * 50;
x.y = position[1] + Math.random() * 50;
});
......
This diff is collapsed.
......@@ -186,7 +186,7 @@ namespace Models {
["hydrophobic", new Array<string>("hydrophobic")],
["aromatic", new Array<string>("aromatic")],
["metal", new Array<string>("metal_complex")],
["clashes", new Array<string>("clash", "vdw_clash")],
["clashes", new Array<string>("clash", "vdw_clash")],
["proximal", new Array<string>("proximal")],
]);
let contacts = this.getTypes();
......@@ -240,7 +240,7 @@ namespace Models {
["hydrophobic", new Array<string>("hydrophobic")],
["aromatic", new Array<string>("aromatic")],
["metal", new Array<string>("metal_complex")],
["clashes", new Array<string>("clash", "vdw_clash")]
["clashes", new Array<string>("clash", "vdw_clash")]
]);
......@@ -296,9 +296,9 @@ namespace Models {
let relevantContacts = this.contacts
.filter(x => (x.partnerA.residue.equals(r) || x.partnerB.residue.equals(r)) && !x.isBoundMoleculePart); //
relevantContacts.forEach(x => {
let atomNode: AtomNode = undefined;
let residueNode: ResidueNode = undefined;
......
......@@ -25,15 +25,29 @@ class Depiction {
return this;
}
public getInitalAtomPosition(atomName: string) {
let thisAtom = this.atoms.find(x => x.name === atomName);
public getInitalAtomPosition(atomNames: string[]) {
if (this.atoms.length === 1) {
return [this.atoms[0].x, this.atoms[0].y]
}
// ideally we want to find an atom which is part just a single bond to get nice initial position.
// If there is no such atom any will do
let searchStruct = new Map<string, number>();
this.bonds.forEach(x => {
searchStruct.set(x.bgn.name, searchStruct.get(x.bgn.name) === undefined ? 1 : searchStruct.get(x.bgn.name) + 1);
searchStruct.set(x.end.name, searchStruct.get(x.end.name) === undefined ? 1 : searchStruct.get(x.end.name) + 1);
});
searchStruct = this.sortMap(searchStruct); // ascending order so we hit the those with less partners sooner.
let thisAtomName = [...searchStruct.keys()].find(x => atomNames.findIndex(y => y === x) !== -1)
let thisAtom = this.atoms.find(x => x.name === thisAtomName);
let bond = this.bonds.find(x => x.containsAtom(thisAtom));
let otherAtom = bond.getOtherAtom(thisAtom);
let v = [otherAtom.x - (2* (otherAtom.x - thisAtom.x)) , otherAtom.y - 2*((otherAtom.y - thisAtom.y))];
return v;
let otherAtom = bond.getOtherAtom(thisAtom);
// to place the residue node a bond apart from the bondin atom
let v = [otherAtom.x - (2 * (otherAtom.x - thisAtom.x)), otherAtom.y - 2 * ((otherAtom.y - thisAtom.y))];
return v;
}
......@@ -63,6 +77,22 @@ class Depiction {
}
});
}
public sortMap(map: Map<string, number>) {
let vals = [...map.values()].sort();
let newMap = new Map<string, number>();
vals.forEach(x => {
map.forEach((value, key) => {
if (x === value) {
newMap.set(key, x);
return;
}
})
});
return newMap;
}
}
......@@ -95,11 +125,11 @@ class Bond {
public getOtherAtom(other: Atom) {
if (!this.bgn.equals(other) && !this.end.equals(other)) throw new Error(`Atom ${other.name} is not a part of the bond.`);
return this.bgn.equals(other) ? this.end : this.bgn;
}
public containsAtom(other: Atom) {
public containsAtom(other: Atom) {
return this.bgn.equals(other) || this.end.equals(other);
}
}
......
......@@ -15,7 +15,7 @@ class Visualization {
private nodes: any;
private links: any
// #endregion
// #region ui properties
private screenshotBtn: d3.Selection<d3.BaseType, {}, HTMLElement, any>;
private centerBtn: d3.Selection<d3.BaseType, {}, HTMLElement, any>;
......@@ -28,7 +28,7 @@ class Visualization {
// #region data properties
private pdbId: string;
private glycanMapper: GlycanMapper;
private selectedBindingSite: Models.BindingSite;
private selectedBindingSite: Models.BindingSite;
// #endregion
// #region handlers
......@@ -72,10 +72,10 @@ class Visualization {
.append('svg')
.attr('xmlns', 'http://www.w3.org/2000/svg')
.attr('width', () => this.width)
.attr('height', () => this.height);
.attr('height', () => this.height);
this.visualization = this.svg.append('g').attr('id', 'vis-root');
this.glycanMapper = new GlycanMapper('het_mapping.json', 'glycans.xml');
this.visualization = this.svg.append('g').attr('id', 'vis-root');
this.glycanMapper = new GlycanMapper('het_mapping.json', 'glycans.xml');
}
......@@ -89,14 +89,14 @@ class Visualization {
d3.json(`${this.pdbId}.json`)
.catch(e => { throw e; })
.then((data: any) => {
this.selectedBindingSite = new Models.BindingSite(data.boundMolecules[i]);
.then((data: any) => {
this.selectedBindingSite = new Models.BindingSite(data.boundMolecules[i]);
this.setupScene();
});
}
private saveSvg(ctx: Visualization) {
private saveSvg(ctx: Visualization) {
d3.text('svgstyles.css').then(x => {
let svgToDl = d3.select('#int-canvas');
svgToDl.select('svg')
......@@ -116,11 +116,11 @@ class Visualization {
});
}
private downloadInteractionsData(ctx: Visualization): void {
let downloadLink = document.createElement('a');
let dataBlob = new Blob([JSON.stringify(ctx.selectedBindingSite.rawData, null, 4)], { type: 'application/json' });
let dataBlob = new Blob([JSON.stringify(ctx.selectedBindingSite.rawData, null, 4)], { type: 'application/json' });
downloadLink.href = URL.createObjectURL(dataBlob);
downloadLink.download = `${ctx.pdbId}_interactions_data.json`;
......@@ -160,9 +160,9 @@ class Visualization {
//https://stackoverflow.com/questions/40722344/understanding-d3-with-an-example-mouseover-mouseup-with-multiple-arguments
private nodeMouseOver(x: Models.ResidueNode, i: number, g: any) {
private nodeMouseOver(x: Models.ResidueNode, i: number, g: any) {
x.scale = 1.5;
d3.select(g[i])
.attr('transform', () => `translate(${x.x},${x.y}) scale(${x.scale})`)
......@@ -186,19 +186,19 @@ class Visualization {
// #region Labels
private resize(ctx: Visualization, isBoundMolecule: boolean) {
private resize(ctx: Visualization, isBoundMolecule: boolean) {
ctx.width = document.getElementById('int-canvas').clientWidth;
ctx.height = window.innerHeight;
ctx.svg.attr('width', ctx.width).attr('height', ctx.height);
if (isBoundMolecule) {
ctx.simulation.force('center', d3.forceCenter(ctx.width / 2, ctx.height / 2))
.restart();
} else {
} else {
ctx.simulation.restart();
}
ctx.zoom_handler(ctx.svg);
}
......@@ -234,12 +234,12 @@ class Visualization {
this.nodeMouseOut(n, i, g);
this.showLabel(n);
let depiction = await new Depiction().load(n.residue.label_comp_id);
let depiction = await new Depiction().load(n.residue.label_comp_id);
let xShift = (this.width / 2) - (depiction.x / 2);
let yShift = (this.height / 2) - (depiction.y / 2);
let visuals = this.selectedBindingSite.getResidueInteractions(n.residue, depiction.atoms);
let visuals = this.selectedBindingSite.getResidueInteractions(n.residue, depiction.atoms);
this.visualization = this.svg.append('g')
.attr('id', 'vis-root')
......@@ -256,36 +256,36 @@ class Visualization {
this.tooltip.transition()
.duration(500)
.style('opacity', 0);
});
});
depiction.appendBondVisualsTo(this.visualization);
depiction.appendTextsTo(this.visualization);
depiction.appendTextsTo(this.visualization);
visuals.nodes.forEach(x => {
if (!(x instanceof Models.ResidueNode)) return;
let lnk = visuals.links.find(y => y.source.equals(x));
let position = depiction.getInitalAtomPosition(lnk.target.atomName);
let lnks = visuals.links.filter(y => y.source.equals(x)).map(x => x.target.atomName);
let position = depiction.getInitalAtomPosition(lnks);
x.x = position[0] + Math.random() * 50;
x.y = position[1]+ Math.random() * 50;
x.y = position[1] + Math.random() * 50;
});
//# region copy-paste
//setup nodes
this.nodes = this.visualization.append('g')
.selectAll()
.data(visuals.nodes)
.enter().append('g');
this.nodes.filter(x => x instanceof Models.ResidueNode)
.enter().append('g');
this.nodes.filter(x => x instanceof Models.ResidueNode)
.attr('class', (e: Models.ResidueNode) => `svg-node svg-${e.residue.getResidueType()}-res`)
.on('mouseover', (x: Models.ResidueNode, i: number, g: any) => this.nodeMouseOver(x, i, g))
.on('mouseout', (x: Models.ResidueNode, i: number, g: any) => context.nodeMouseOut(x, i, g));
// draw glycans
this.nodes.filter(x => x instanceof Models.ResidueNode)
.filter((x: Models.ResidueNode) => this.glycanMapper.mapping.has(x.residue.label_comp_id))
......@@ -296,7 +296,7 @@ class Visualization {
.filter(x => x instanceof Models.ResidueNode)
.filter(x => !this.glycanMapper.mapping.has(x.residue.label_comp_id))
.append('circle')
.attr('r', '25');
.attr('r', '25');
this.nodes
.filter(x => x instanceof Models.ResidueNode)
......@@ -313,28 +313,28 @@ class Visualization {
.text(labels[i]);
}
});
// let forceLink = d3.forceLink()
// .links(visuals.links)
// .distance(150)
// .strength(0.5);
// let forceLink = d3.forceLink()
// .links(visuals.links)
// .distance(150)
// .strength(0.5);
//let charge = d3.forceManyBody().strength(-1000).distanceMin(50).distanceMax(150);
let collision = d3.forceCollide().radius(50)
//let center = d3.forceCenter(xShift, yShift);
this.simulation = d3.forceSimulation(visuals.nodes)
// .force('link', forceLink)
// .force('charge', charge) //strength
// .force('link', forceLink)
// .force('charge', charge) //strength
.force('collision', collision)
// .force('center', center)
.on('tick', () => this.simulationStep(this));
// .force('center', center)
.on('tick', () => this.simulationStep(this));
this.drag_handler(this.nodes);
this.zoom_handler(this.svg);
//#endregion
d3.select(window).on('resize', () => this.resize(context, false));
d3.select(window).on('resize', () => this.resize(context, false));
}
......@@ -367,9 +367,9 @@ class Visualization {
let pivot = visuals.nodes.filter((x: Models.ResidueNode) => x.residue.isLigand)[0];
this.selectLigand(pivot, 0, this.nodes);
}
this.nodes.filter((n: Models.ResidueNode) =>
this.glycanMapper.mapping.has(n.residue.label_comp_id))
.html((e: Models.ResidueNode) => this.glycanMapper.getGlycanImage(e.residue.label_comp_id));
......@@ -400,7 +400,7 @@ class Visualization {
let charge = d3.forceManyBody().strength(-1000).distanceMin(55).distanceMax(250);
let collision = d3.forceCollide(45);
let center = d3.forceCenter(this.width / 2, this.height / 2);
this.simulation = d3.forceSimulation(visuals.nodes)
.force('link', forceLink)
.force('charge', charge) //strength
......@@ -410,10 +410,10 @@ class Visualization {
this.drag_handler(this.nodes);
this.zoom_handler(this.svg);
d3.select(window).on('resize', () => this.resize(context, true));
d3.select(window).on('resize', () => this.resize(context, true));
}
private simulationStep(ctx: Visualization) {
ctx.nodes.attr('transform', (d) => `translate(${d.x},${d.y}) scale(${d.scale})`);
ctx.links.attr('x1', (x: any) => x.source.x)
......@@ -422,10 +422,10 @@ class Visualization {
.attr('y2', (x: any) => x.target.y);
}
private centerScene(ctx: Visualization){
private centerScene(ctx: Visualization) {
if (ctx.nodes.length === 0)
return;
return;
// Get the bounding box
let minX: any = d3.min(ctx.nodes.data().map((x) => x.x));
let minY: any = d3.min(ctx.nodes.data().map((x) => x.y));
......@@ -433,7 +433,7 @@ class Visualization {
let maxX: any = d3.max(ctx.nodes.data().map((x) => x.x));
let maxY: any = d3.max(ctx.nodes.data().map((x) => x.y));
// The width and the height of the graph
let molWidth = maxX - minX;
let molHeight = maxY - minY;
......@@ -458,8 +458,8 @@ class Visualization {
// do the actual moving
ctx.visualization.attr("transform", `translate(${x_trans}, ${y_trans}) scale(${min_ratio})`);
// tell the zoomer what we did so that next we zoom, it uses the
// transformation we entered here
// tell the zoomer what we did so that next we zoom, it uses the
// transformation we entered here
//ctx.zoom_handler.(ctx.svg, x_trans, y_trans);
//ctx.zoom_handler.scaleTo(ctx.svg, min_ratio);
......
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