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
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
        this.appendBondVisuals();
        this.appendTexts();
    }

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

        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
106

Lukas Pravda's avatar
Lukas Pravda committed
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
        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
126
    public addContour(data: any) {
127
128
129
130
        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.labels).length !== 0).map(x => x.labels).reduce((a, b) => a.concat(b)))
159
160
161
162
163
            .enter()
            .append('text')
            .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
164
165
            .attr('dominant-baseline', (x: any) => x['dominant-baseline'])
            .attr('text-anchor', (x: any) => x['text-anchor'])
166
167
168
169
170
171
172
173
            .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
174
    }
Lukas Pravda's avatar
Lukas Pravda committed
175

176
177
178
179
180
181
    /**
     * 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
182
183
    private appendClarityNodes(): void {
        this.structure.selectAll()
Lukas Pravda's avatar
Lukas Pravda committed
184
            .data(this.atoms.filter(x => Object.keys(x.labels).length != 0))
185
            .enter().append('circle')
Lukas Pravda's avatar
Lukas Pravda committed
186
            .classed('pdb-lig-env-svg-shadow-node', true)
187
188
            .attr('cx', (x: any) => x.position.x)
            .attr('cy', (x: any) => x.position.y)
189
            .attr('r', 15);
190
191
    }

Lukas Pravda's avatar
Lukas Pravda committed
192

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

196
        ids.forEach(x => {
Lukas Pravda's avatar
Lukas Pravda committed
197
198
199
            let pos = this.atoms.find(y => y.name === x).position;
            coords.push(pos);
        })
200

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

231

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

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

        return other.name === this.name;
Lukas Pravda's avatar
Lukas Pravda committed
261
262
263
    }
}

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

Lukas Pravda's avatar
Lukas Pravda committed
275
276

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

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

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

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

Lukas Pravda's avatar
Lukas Pravda committed
304
305
306
307
308
309
310
311
312

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

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

    /**
     * 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
327
328
329
        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
330
        return new Vector2D(x, y);
Lukas Pravda's avatar
Lukas Pravda committed
331
332
333
    }
}

Lukas Pravda's avatar
Lukas Pravda committed
334

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

350

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

Lukas Pravda's avatar
Lukas Pravda committed
364
365
366
367
368
369
370
371
372

    /**
     * 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
373
    public getOtherAtom(other: Atom): Atom {
374
        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
375

376
377
378
        return this.bgn.equals(other) ? this.end : this.bgn;
    }

Lukas Pravda's avatar
Lukas Pravda committed
379
380
381
382
383
384
385
386

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

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