depiction.ts 12.2 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
11
 * @param {string} ccdId PDB CCD id.
 * @param {Atom[]} atoms List of atoms.
 * @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
Lukas Pravda's avatar
Lukas Pravda committed
14
15
 * for a scene shift, so it is centered. 
 */
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
41
42
43
44
45
46
47
        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>();

        data.bonds.forEach(x => {
            var atomA = this.atoms.find(e => e.name == x.bgn);
            var atomB = this.atoms.find(e => e.name == x.end);
            var bond = new Bond(atomA, atomB, x.coords, x.style);

            this.bonds.push(bond);
        });
Lukas Pravda's avatar
Lukas Pravda committed
48
49
    }

50

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

Lukas Pravda's avatar
Lukas Pravda committed
72
73
74
75
        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);
        });
Lukas Pravda's avatar
Lukas Pravda committed
76
        searchStruct = this.sortMap(searchStruct); // ascending order so we hit those with less partners sooner.
Lukas Pravda's avatar
Lukas Pravda committed
77
78
79
80

        let thisAtomName = [...searchStruct.keys()].find(x => atomNames.findIndex(y => y === x) !== -1)
        let thisAtom = this.atoms.find(x => x.name === thisAtomName);

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

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

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

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

94
        if (withClarityNodes) this.appendClarityNodes();
Lukas Pravda's avatar
Lukas Pravda committed
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
        this.appendBondVisuals();
        this.appendTexts();
    }

    public highlightSubgraph(atoms: Array<string>, color: string = undefined) {
        if (!this.atoms || !atoms) return;        

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

        color = color ? color : "#BFBFBF";
        let atomsToHighlight = this.atoms.filter(x => atoms.includes(x.name));
        
        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`)
    }

126
127
128
129
130
    public addContour(data: any) { 
        this.contour.selectAll('*').remove();

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

Lukas Pravda's avatar
Lukas Pravda committed
132
133
134
135
136
137
    /**
     * 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
138
139
    private appendBondVisuals(): void {
        this.structure.selectAll()
Lukas Pravda's avatar
Lukas Pravda committed
140
            .data(this.bonds)
Lukas Pravda's avatar
Lukas Pravda committed
141
142
            .enter()
            .append('path')
Lukas Pravda's avatar
Lukas Pravda committed
143
144
            .attr('style', (y: Bond) => y.style)
            .attr('d', (y: Bond) => y.coords);
Lukas Pravda's avatar
Lukas Pravda committed
145
146
    }

Lukas Pravda's avatar
Lukas Pravda committed
147
148
149
150
    /**
     * 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.
Lukas Pravda's avatar
Lukas Pravda committed
151
     * Also there are all sorts of colorful subscripts and superscripts, 
Lukas Pravda's avatar
Lukas Pravda committed
152
153
154
155
     * so it is much easier to use it this way.
     *
     * @memberof Depiction
     */
Lukas Pravda's avatar
Lukas Pravda committed
156
157
    private appendTexts(): void {
        this.structure.selectAll()
Lukas Pravda's avatar
Lukas Pravda committed
158
            .data(this.atoms.filter(x => Object.keys(x.label).length !== 0).map(x => x.label))
159
160
161
162
163
164
165
166
167
168
169
170
171
            .enter()
            .append('text')
            .attr('style', (x: any) => x.style)
            .attr('x', (x: any) => x.x)
            .attr('y', (x: any) => x.y)
            .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
172
    }
Lukas Pravda's avatar
Lukas Pravda committed
173

174
175
176
177
178
179
    /**
     * Add small white circle on the background of atoms with label
     * just to make the interaction lines pretty.
     *
     * @memberof Depiction
     */
Lukas Pravda's avatar
Lukas Pravda committed
180
181
    private appendClarityNodes(): void {
        this.structure.selectAll()
182
183
            .data(this.atoms.filter(x => Object.keys(x.label).length != 0))
            .enter().append('circle')
Lukas Pravda's avatar
Lukas Pravda committed
184
            .classed('pdbe-int-svg-shadow-node', true)
185
186
            .attr('cx', (x: any) => x.position.x)
            .attr('cy', (x: any) => x.position.y)
187
            .attr('r', 15);
188
189
    }

Lukas Pravda's avatar
Lukas Pravda committed
190

Lukas Pravda's avatar
Lukas Pravda committed
191
    public getCenter(ids: string[]): Vector2D {
Lukas Pravda's avatar
Lukas Pravda committed
192
        let coords = new Array<Vector2D>();
193
        
194
        ids.forEach(x => {
Lukas Pravda's avatar
Lukas Pravda committed
195
196
197
            let pos = this.atoms.find(y => y.name === x).position;
            coords.push(pos);
        })
198

Lukas Pravda's avatar
Lukas Pravda committed
199
200
201
202
203
204
        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
205
206
207
208
209
210
211
    /**
     *
     *
     * @param {Map<string, number>} map
     * @returns
     * @memberof Depiction
     */
Lukas Pravda's avatar
Lukas Pravda committed
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
    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
227
228
}

229

Lukas Pravda's avatar
Lukas Pravda committed
230
/**
Lukas Pravda's avatar
Lukas Pravda committed
231
 * Atom from the depiction
Lukas Pravda's avatar
Lukas Pravda committed
232
233
234
 *
 * @class Atom
 * @param {string} name Unique atom name.
Lukas Pravda's avatar
Lukas Pravda committed
235
 * @param {any} label Atom label
Lukas Pravda's avatar
Lukas Pravda committed
236
237
 * @param {Vector2D} position Position of the atom in 2D coordinate system.
 */
238
class Atom {
Lukas Pravda's avatar
Lukas Pravda committed
239
    name: string;
Lukas Pravda's avatar
Lukas Pravda committed
240
    label: any;
Lukas Pravda's avatar
Lukas Pravda committed
241
    position: Vector2D;
Lukas Pravda's avatar
Lukas Pravda committed
242
243
    constructor(item: any) {
        this.name = item.name;
Lukas Pravda's avatar
Lukas Pravda committed
244
245
        this.label = item.label;
        this.position = new Vector2D(item.x, item.y)
246
247
    }

Lukas Pravda's avatar
Lukas Pravda committed
248
249
250
251
252
253
254
    /**
     * 
     *
     * @param {Atom} other
     * @returns true if the atoms are equal
     * @memberof Atom
     */
255
256
257
258
    public equals(other: Atom) {
        if (!(other instanceof Atom)) return false;

        return other.name === this.name;
Lukas Pravda's avatar
Lukas Pravda committed
259
260
261
    }
}

Lukas Pravda's avatar
Lukas Pravda committed
262
/**
Lukas Pravda's avatar
Lukas Pravda committed
263
 * 2D point definition
Lukas Pravda's avatar
Lukas Pravda committed
264
265
266
267
268
269
 *
 * @class Point
 * @param {number} x coordinate
 * @param {number} y coordinate
 */
class Vector2D {
Lukas Pravda's avatar
Lukas Pravda committed
270
271
272
    x: number;
    y: number;

Lukas Pravda's avatar
Lukas Pravda committed
273
274

    constructor(x: number, y: number) {
Lukas Pravda's avatar
Lukas Pravda committed
275
        this.x = x;
Lukas Pravda's avatar
Lukas Pravda committed
276
        this.y = y;
Lukas Pravda's avatar
Lukas Pravda committed
277
278
    }

Lukas Pravda's avatar
Lukas Pravda committed
279
280
281
282
283
284
285
    /**
     * 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
286
        return `[${this.x}, ${this.y}]`;
Lukas Pravda's avatar
Lukas Pravda committed
287
288
    }

Lukas Pravda's avatar
Lukas Pravda committed
289
    /**
Lukas Pravda's avatar
Lukas Pravda committed
290
     * Checks whether or not two Vector2D objects are equal.
Lukas Pravda's avatar
Lukas Pravda committed
291
292
293
294
295
296
297
     *
     * @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
298
299
300
301

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

Lukas Pravda's avatar
Lukas Pravda committed
302
303
304
305
306
307
308
309
310

    /**
     * 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
311
312
313
        return Math.sqrt(Math.pow(other.x - this.x, 2) + Math.pow(other.y - this.x, 2));
    }

Lukas Pravda's avatar
Lukas Pravda committed
314
315
316
317
318
319
320
321
322
323
324

    /**
     * Composes vectors to a single one. This is used in infering the 
     * 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
325
326
327
        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
328
        return new Vector2D(x, y);
Lukas Pravda's avatar
Lukas Pravda committed
329
330
331
    }
}

Lukas Pravda's avatar
Lukas Pravda committed
332

Lukas Pravda's avatar
Lukas Pravda committed
333
334
335
336
/**
 * Represents a bond in a 2D depiction.
 *
 * @class Bond
Lukas Pravda's avatar
Lukas Pravda committed
337
338
339
340
 * @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
341
 */
342
343
344
class Bond {
    bgn: Atom;
    end: Atom;
Lukas Pravda's avatar
Lukas Pravda committed
345
346
347
    coords: string;
    style: string;

348

Lukas Pravda's avatar
Lukas Pravda committed
349
350
351
352
353
354
    /**
     *Creates an instance of the bond.
     * @param {Atom} a
     * @param {Atom} b
     * @memberof Bond
     */
Lukas Pravda's avatar
Lukas Pravda committed
355
    constructor(a: Atom, b: Atom, coords: string, style: string) {
356
357
        this.bgn = a;
        this.end = b;
Lukas Pravda's avatar
Lukas Pravda committed
358
359
        this.coords = coords;
        this.style = style.replace("stroke-width:2px", "stroke-width:4px");;
360
361
    }

Lukas Pravda's avatar
Lukas Pravda committed
362
363
364
365
366
367
368
369
370

    /**
     * Get the other atom for a given bond.
     *
     * @param {Atom} other     
     * @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
371
    public getOtherAtom(other: Atom): Atom {
372
        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
373

374
375
376
        return this.bgn.equals(other) ? this.end : this.bgn;
    }

Lukas Pravda's avatar
Lukas Pravda committed
377
378
379
380
381
382
383
384

    /**
     * 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
385
    public containsAtom(other: Atom) {
386
387
388
        return this.bgn.equals(other) || this.end.equals(other);
    }

Lukas Pravda's avatar
Lukas Pravda committed
389
390
391
392
393
394
395
    /**
     * Hide bond from the representation.
     *
     * @memberof Bond
     */
    public hide() {
        this.style.replace("stroke-width:4px", "stroke-width:0px");
396
    }
Lukas Pravda's avatar
Lukas Pravda committed
397
}