depiction.ts 12.4 KB
Newer Older
Lukas Pravda's avatar
Lukas Pravda committed
1 2 3 4 5 6 7 8
/**
 * 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
 * binding partners in the residue-level view.
 *
 * @author Lukas Pravda <lpravda@ebi.ac.uk>
 * @class Depiction
Lukas Pravda's avatar
Lukas Pravda committed
9 10
 * @param {string} ccdId PDB CCD id.
 * @param {Atom[]} atoms List of atoms.
11
 * @param {Bond[]} bonds Visual representation of bonds.
Lukas Pravda's avatar
Lukas Pravda committed
12
 * They do not correlate 1:1 with a number of bonds!
Lukas Pravda's avatar
Lukas Pravda committed
13
 * @param {Vector2D} resolution x,y dimension of the image. Needs to be used
14
 * for a scene shift, so it is centered.
Lukas Pravda's avatar
Lukas Pravda committed
15
 */
Lukas Pravda's avatar
Lukas Pravda committed
16
class Depiction {
Lukas Pravda's avatar
Lukas Pravda committed
17
    ccdId: string;
Lukas Pravda's avatar
Lukas Pravda committed
18 19
    atoms: Atom[];
    bonds: Bond[];
Lukas Pravda's avatar
Lukas Pravda committed
20

Lukas Pravda's avatar
Lukas Pravda committed
21
    resolution: Vector2D;
22

Lukas Pravda's avatar
Lukas Pravda committed
23 24 25 26 27 28 29 30 31 32 33 34
    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>;

    constructor(parent: any, data: any) {
        this.root = parent

        this.highlight = this.root.append('g').attr('id', 'highlight');
        this.structure = this.root.append('g').attr('id', 'structure');
        this.contour = this.root.append('g').attr('id', 'contour');

35 36 37 38 39 40
        this.ccdId = data.ccd_id;
        this.resolution = new Vector2D(data.resolution.x, data.resolution.y);

        this.atoms = data.atoms.map(x => new Atom(x));
        this.bonds = new Array<Bond>();

Lukas Pravda's avatar
Lukas Pravda committed
41
        let bds = new Set<string>();
42

43
        data.bonds.forEach(x => {
Lukas Pravda's avatar
Lukas Pravda committed
44 45 46 47 48 49 50 51 52 53
            let atomA = this.atoms.find(e => e.name == x.bgn);
            let atomB = this.atoms.find(e => e.name == x.end);
            let bond = new Bond(atomA, atomB, x.coords, x.style);

            let bondFlag = [atomA.name, atomB.name].sort().join("_");
            if (!bds.has(bondFlag)) {
                bds.add(bondFlag);
                atomA.connectivity++;
                atomB.connectivity++;
            }
54 55 56

            this.bonds.push(bond);
        });
Lukas Pravda's avatar
Lukas Pravda committed
57 58
    }

59

Lukas Pravda's avatar
Lukas Pravda committed
60
    /**
61
     * Returns an initial position of Residue node bound to a list of
Lukas Pravda's avatar
Lukas Pravda committed
62
     * atom.
63
     *
Lukas Pravda's avatar
Lukas Pravda committed
64
     * Present implementation sorts all the partners based on the atom
65
     * degree and then gets the one with the lovest degree and places
Lukas Pravda's avatar
Lukas Pravda committed
66 67
     * the initial residue position along the vector pointing from it.
     *
68
     * @param {string[]} atomNames list of atom names the bound residue
Lukas Pravda's avatar
Lukas Pravda committed
69 70 71 72
     * has a contact with.
     * @returns {Vector2D} Returns an initial placement of the residue in contact.
     * @memberof Depiction
     */
Lukas Pravda's avatar
Lukas Pravda committed
73
    public getInitalNodePosition(atomNames: string[]): Vector2D {
Lukas Pravda's avatar
Lukas Pravda committed
74
        if (this.atoms.length === 1) {
Lukas Pravda's avatar
Lukas Pravda committed
75
            return new Vector2D(this.atoms[0].position.x, this.atoms[0].position.y);
Lukas Pravda's avatar
Lukas Pravda committed
76 77 78 79
        }
        // 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

Lukas Pravda's avatar
Lukas Pravda committed
80 81
        let atoms = this.atoms.filter(x => atomNames.includes(x.name)).sort((x, y) => x.connectivity - y.connectivity);
        let thisAtom = atoms[0];
Lukas Pravda's avatar
Lukas Pravda committed
82

83
        let bond = this.bonds.find(x => x.containsAtom(thisAtom));
Lukas Pravda's avatar
Lukas Pravda committed
84
        let otherAtom = bond.getOtherAtom(thisAtom);
85

Lukas Pravda's avatar
Lukas Pravda committed
86 87
        // to place the residue node a bond apart from the bonding atom
        let x = otherAtom.position.x - (2 * (otherAtom.position.x - thisAtom.position.x))
88
        let y = otherAtom.position.y - 2 * ((otherAtom.position.y - thisAtom.position.y))
89

Lukas Pravda's avatar
Lukas Pravda committed
90
        return new Vector2D(x, y);
91 92
    }

93
    public draw(atom_names: boolean = false) {
Lukas Pravda's avatar
Lukas Pravda committed
94
        this.structure.selectAll("*").remove();
95

Lukas Pravda's avatar
Lukas Pravda committed
96
        this.appendBondVisuals();
97 98 99

        if (atom_names) this.appendAtomNames();
        else this.appendLabels();
Lukas Pravda's avatar
Lukas Pravda committed
100 101 102
    }

    public highlightSubgraph(atoms: Array<string>, color: string = undefined) {
Lukas Pravda's avatar
Lukas Pravda committed
103
        if (!this.atoms || !atoms) return;
Lukas Pravda's avatar
Lukas Pravda committed
104 105 106 107 108

        this.highlight.selectAll('*').remove();

        color = color ? color : "#BFBFBF";
        let atomsToHighlight = this.atoms.filter(x => atoms.includes(x.name));
Lukas Pravda's avatar
Lukas Pravda committed
109

Lukas Pravda's avatar
Lukas Pravda committed
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
        this.highlight.selectAll()
            .data(atomsToHighlight)
            .enter()
            .append('circle')
            .attr('r', '16.12')
            .attr('cx', x => x.position.x)
            .attr('cy', x => x.position.y)
            .attr('style', `fill:${color};fill-rule:evenodd;stroke:${color};stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1`);

        let bondsToHighlight = this.bonds.filter(x => atoms.includes(x.bgn.name) && atoms.includes(x.end.name))

        this.highlight.selectAll()
            .data(bondsToHighlight)
            .enter()
            .append('path')
            .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`)
    }

Lukas Pravda's avatar
Lukas Pravda committed
129
    public addContour(data: any) {
130 131 132 133
        this.contour.selectAll('*').remove();

        this.contour.append('div').text(`'contour data goes here: ${data}`);
    }
134

Lukas Pravda's avatar
Lukas Pravda committed
135 136 137 138 139 140
    /**
     * Appends to a given selection the visual representation of bonds as svg:path elements.
     *
     * representation of the bond visuals.
     * @memberof Depiction
     */
Lukas Pravda's avatar
Lukas Pravda committed
141 142
    private appendBondVisuals(): void {
        this.structure.selectAll()
Lukas Pravda's avatar
Lukas Pravda committed
143
            .data(this.bonds)
Lukas Pravda's avatar
Lukas Pravda committed
144 145
            .enter()
            .append('path')
Lukas Pravda's avatar
Lukas Pravda committed
146 147
            .attr('style', (y: Bond) => y.style)
            .attr('d', (y: Bond) => y.coords);
Lukas Pravda's avatar
Lukas Pravda committed
148 149
    }

150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
    /**
     * Append atom name labels to the visualization.
     *
     * @memberof Depiction
     */
    private appendAtomNames() {
        this.structure.selectAll()
            .data(this.atoms)
            .enter()
            .append('text')
            .attr('filter', "url(#solid-background)")
            .attr('style', 'font-size:21px;font-style:normal;font-weight:normal;fill-opacity:1;stroke:none;font-family:sans-serif;fill:#000000')
            .attr('x', x => x.position.x)
            .attr('y', x => x.position.y)
            .attr('dominant-baseline', 'central')
            .attr('text-anchor', 'middle')
            .text(x => x.name);
    }

Lukas Pravda's avatar
Lukas Pravda committed
169 170 171 172
    /**
     * 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.
173
     * Also there are all sorts of colorful subscripts and superscripts,
Lukas Pravda's avatar
Lukas Pravda committed
174 175 176 177
     * so it is much easier to use it this way.
     *
     * @memberof Depiction
     */
178
    private appendLabels() {
Lukas Pravda's avatar
Lukas Pravda committed
179 180 181 182 183
        let data = this.atoms
            .filter(x => x.labels.length > 0)
            ?.map(x => x.labels)
            ?.reduce((a, b) => a.concat(b));

Lukas Pravda's avatar
Lukas Pravda committed
184
        this.structure.selectAll()
Lukas Pravda's avatar
Lukas Pravda committed
185
            .data(data)
186 187
            .enter()
            .append('text')
188
            .attr('filter', "url(#solid-background)")
189 190 191
            .attr('style', (x: any) => x.style)
            .attr('x', (x: any) => x.x)
            .attr('y', (x: any) => x.y)
Lukas Pravda's avatar
Lukas Pravda committed
192 193
            .attr('dominant-baseline', (x: any) => x['dominant-baseline'])
            .attr('text-anchor', (x: any) => x['text-anchor'])
194 195 196 197 198 199 200 201
            .each(function (x: any) {
                for (var i = 0; i < x.tspans.length; i++) {
                    d3.select(this)
                        .append('tspan')
                        .attr('style', x.tspans[i].style)
                        .text(x.tspans[i].value);
                }
            });
Lukas Pravda's avatar
Lukas Pravda committed
202
    }
Lukas Pravda's avatar
Lukas Pravda committed
203

Lukas Pravda's avatar
Lukas Pravda committed
204
    public getCenter(ids: string[]): Vector2D {
Lukas Pravda's avatar
Lukas Pravda committed
205
        let coords = new Array<Vector2D>();
Lukas Pravda's avatar
Lukas Pravda committed
206

207
        ids.forEach(x => {
Lukas Pravda's avatar
Lukas Pravda committed
208 209 210
            let pos = this.atoms.find(y => y.name === x).position;
            coords.push(pos);
        })
211

Lukas Pravda's avatar
Lukas Pravda committed
212 213 214 215 216 217
        let x = d3.sum(coords, x => x.x) / coords.length;
        let y = d3.sum(coords, x => x.y) / coords.length;

        return new Vector2D(x, y);
    }

Lukas Pravda's avatar
Lukas Pravda committed
218 219 220 221 222 223 224
    /**
     *
     *
     * @param {Map<string, number>} map
     * @returns
     * @memberof Depiction
     */
Lukas Pravda's avatar
Lukas Pravda committed
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
    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;
    }
Lukas Pravda's avatar
Lukas Pravda committed
240 241
}

242

Lukas Pravda's avatar
Lukas Pravda committed
243
/**
Lukas Pravda's avatar
Lukas Pravda committed
244
 * Atom from the depiction
Lukas Pravda's avatar
Lukas Pravda committed
245 246 247
 *
 * @class Atom
 * @param {string} name Unique atom name.
Lukas Pravda's avatar
Lukas Pravda committed
248
 * @param {any} labels Atom label
Lukas Pravda's avatar
Lukas Pravda committed
249 250
 * @param {Vector2D} position Position of the atom in 2D coordinate system.
 */
251
class Atom {
Lukas Pravda's avatar
Lukas Pravda committed
252
    name: string;
Lukas Pravda's avatar
Lukas Pravda committed
253
    labels: any;
Lukas Pravda's avatar
Lukas Pravda committed
254
    position: Vector2D;
Lukas Pravda's avatar
Lukas Pravda committed
255 256
    connectivity: number

Lukas Pravda's avatar
Lukas Pravda committed
257 258
    constructor(item: any) {
        this.name = item.name;
Lukas Pravda's avatar
Lukas Pravda committed
259
        this.labels = item.labels;
Lukas Pravda's avatar
Lukas Pravda committed
260 261
        this.position = new Vector2D(item.x, item.y);
        this.connectivity = 0;
262 263
    }

Lukas Pravda's avatar
Lukas Pravda committed
264
    /**
265
     *
Lukas Pravda's avatar
Lukas Pravda committed
266 267 268 269 270
     *
     * @param {Atom} other
     * @returns true if the atoms are equal
     * @memberof Atom
     */
271 272 273 274
    public equals(other: Atom) {
        if (!(other instanceof Atom)) return false;

        return other.name === this.name;
Lukas Pravda's avatar
Lukas Pravda committed
275 276 277
    }
}

Lukas Pravda's avatar
Lukas Pravda committed
278
/**
Lukas Pravda's avatar
Lukas Pravda committed
279
 * 2D point definition
Lukas Pravda's avatar
Lukas Pravda committed
280 281 282 283 284 285
 *
 * @class Point
 * @param {number} x coordinate
 * @param {number} y coordinate
 */
class Vector2D {
Lukas Pravda's avatar
Lukas Pravda committed
286 287 288
    x: number;
    y: number;

Lukas Pravda's avatar
Lukas Pravda committed
289 290

    constructor(x: number, y: number) {
Lukas Pravda's avatar
Lukas Pravda committed
291
        this.x = x;
Lukas Pravda's avatar
Lukas Pravda committed
292
        this.y = y;
Lukas Pravda's avatar
Lukas Pravda committed
293 294
    }

Lukas Pravda's avatar
Lukas Pravda committed
295 296 297 298 299 300 301
    /**
     * Returns a string representation of the object in a format: [x, y]
     *
     * @returns {string} String representation of the object
     * @memberof Point
     */
    public toString(): string {
Lukas Pravda's avatar
Lukas Pravda committed
302
        return `[${this.x}, ${this.y}]`;
Lukas Pravda's avatar
Lukas Pravda committed
303 304
    }

Lukas Pravda's avatar
Lukas Pravda committed
305
    /**
Lukas Pravda's avatar
Lukas Pravda committed
306
     * Checks whether or not two Vector2D objects are equal.
Lukas Pravda's avatar
Lukas Pravda committed
307 308 309 310 311 312 313
     *
     * @param {Vector2D} other instance of an object to check.
     * @returns {boolean} whether or not the objects are equal.
     * @memberof Point
     */
    public equals(other: Vector2D): boolean {
        if (!(other instanceof Vector2D)) return false;
Lukas Pravda's avatar
Lukas Pravda committed
314 315 316 317

        return this.x == other.x && this.y == other.y;
    }

Lukas Pravda's avatar
Lukas Pravda committed
318 319 320 321 322 323 324 325 326

    /**
     * Measures a distance between this atom and another atom.
     *
     * @param {Vector2D} other atom to measure a distance to.
     * @returns {number} Returns the distance to another object.
     * @memberof Point
     */
    public distanceTo(other: Vector2D): number {
Lukas Pravda's avatar
Lukas Pravda committed
327 328 329
        return Math.sqrt(Math.pow(other.x - this.x, 2) + Math.pow(other.y - this.x, 2));
    }

Lukas Pravda's avatar
Lukas Pravda committed
330 331

    /**
332
     * Composes vectors to a single one. This is used in infering the
Lukas Pravda's avatar
Lukas Pravda committed
333 334 335 336 337 338 339 340
     * original placement of the residue nodes.
     *
     * @static
     * @param {Vector2D[]} points Vectors to be composed.
     * @returns {Vector2D} Result of a vector composition.
     * @memberof Point
     */
    public static composeVectors(points: Vector2D[]): Vector2D {
Lukas Pravda's avatar
Lukas Pravda committed
341 342 343
        let x = d3.sum(points.map(x => x.x));
        let y = d3.sum(points.map(x => x.y));

Lukas Pravda's avatar
Lukas Pravda committed
344
        return new Vector2D(x, y);
Lukas Pravda's avatar
Lukas Pravda committed
345 346 347
    }
}

Lukas Pravda's avatar
Lukas Pravda committed
348

Lukas Pravda's avatar
Lukas Pravda committed
349 350 351 352
/**
 * Represents a bond in a 2D depiction.
 *
 * @class Bond
Lukas Pravda's avatar
Lukas Pravda committed
353 354 355 356
 * @param {Atom} bgn one side of the bond.
 * @param {Atom} end the other side of the bond.
 * @param {string} coords coordinates of the bonds graphical primitive.
 * @param {string} style Style of the bonds graphical primitive.
Lukas Pravda's avatar
Lukas Pravda committed
357
 */
358 359 360
class Bond {
    bgn: Atom;
    end: Atom;
Lukas Pravda's avatar
Lukas Pravda committed
361 362 363
    coords: string;
    style: string;

Lukas Pravda's avatar
Lukas Pravda committed
364 365 366 367 368 369
    /**
     *Creates an instance of the bond.
     * @param {Atom} a
     * @param {Atom} b
     * @memberof Bond
     */
Lukas Pravda's avatar
Lukas Pravda committed
370
    constructor(a: Atom, b: Atom, coords: string, style: string) {
371 372
        this.bgn = a;
        this.end = b;
Lukas Pravda's avatar
Lukas Pravda committed
373
        this.coords = coords;
Lukas Pravda's avatar
Lukas Pravda committed
374
        this.style = style.replace("stroke-width:2px", "stroke-width:4px");
375 376
    }

Lukas Pravda's avatar
Lukas Pravda committed
377 378 379 380

    /**
     * Get the other atom for a given bond.
     *
381
     * @param {Atom} other
Lukas Pravda's avatar
Lukas Pravda committed
382 383 384 385
     * @returns {Atom} The other atom from the bond.
     * @throws {Error} if the atom is not part of that bond at all.
     * @memberof Bond
     */
Lukas Pravda's avatar
Lukas Pravda committed
386
    public getOtherAtom(other: Atom): Atom {
387
        if (!this.bgn.equals(other) && !this.end.equals(other)) throw new Error(`Atom ${other.name} is not a part of the bond.`);
Lukas Pravda's avatar
Lukas Pravda committed
388

389 390 391
        return this.bgn.equals(other) ? this.end : this.bgn;
    }

Lukas Pravda's avatar
Lukas Pravda committed
392 393 394 395 396 397 398 399

    /**
     * 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.
     * @memberof Bond
     */
Lukas Pravda's avatar
Lukas Pravda committed
400
    public containsAtom(other: Atom) {
401 402 403
        return this.bgn.equals(other) || this.end.equals(other);
    }

Lukas Pravda's avatar
Lukas Pravda committed
404 405 406 407 408 409 410
    /**
     * Hide bond from the representation.
     *
     * @memberof Bond
     */
    public hide() {
        this.style.replace("stroke-width:4px", "stroke-width:0px");
411
    }
Lukas Pravda's avatar
Lukas Pravda committed
412
}