depiction.ts 11.9 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() {
Lukas Pravda's avatar
Lukas Pravda committed
92
        this.structure.selectAll("*").remove();
93

Lukas Pravda's avatar
Lukas Pravda committed
94
95
96
97
98
        this.appendBondVisuals();
        this.appendTexts();
    }

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

        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
105

Lukas Pravda's avatar
Lukas Pravda committed
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
        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
125
    public addContour(data: any) {
126
127
128
129
        this.contour.selectAll('*').remove();

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

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

Lukas Pravda's avatar
Lukas Pravda committed
146
147
148
149
    /**
     * 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
150
     * Also there are all sorts of colorful subscripts and superscripts, 
Lukas Pravda's avatar
Lukas Pravda committed
151
152
153
154
     * so it is much easier to use it this way.
     *
     * @memberof Depiction
     */
Lukas Pravda's avatar
Lukas Pravda committed
155
    private appendTexts(): void {
Lukas Pravda's avatar
Lukas Pravda committed
156
157
158
159
160
161
        
        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
162
        this.structure.selectAll()
Lukas Pravda's avatar
Lukas Pravda committed
163
            .data(data)
164
165
            .enter()
            .append('text')
166
            .attr('filter', "url(#solid-background)")
167
168
169
            .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
170
171
            .attr('dominant-baseline', (x: any) => x['dominant-baseline'])
            .attr('text-anchor', (x: any) => x['text-anchor'])
172
            .each(function (x: any) {
173
                
174
175
176
177
178
179
180
                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
181
    }
Lukas Pravda's avatar
Lukas Pravda committed
182

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

186
        ids.forEach(x => {
Lukas Pravda's avatar
Lukas Pravda committed
187
188
189
            let pos = this.atoms.find(y => y.name === x).position;
            coords.push(pos);
        })
190

Lukas Pravda's avatar
Lukas Pravda committed
191
192
193
194
195
196
        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
197
198
199
200
201
202
203
    /**
     *
     *
     * @param {Map<string, number>} map
     * @returns
     * @memberof Depiction
     */
Lukas Pravda's avatar
Lukas Pravda committed
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
    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
219
220
}

221

Lukas Pravda's avatar
Lukas Pravda committed
222
/**
Lukas Pravda's avatar
Lukas Pravda committed
223
 * Atom from the depiction
Lukas Pravda's avatar
Lukas Pravda committed
224
225
226
 *
 * @class Atom
 * @param {string} name Unique atom name.
Lukas Pravda's avatar
Lukas Pravda committed
227
 * @param {any} labels Atom label
Lukas Pravda's avatar
Lukas Pravda committed
228
229
 * @param {Vector2D} position Position of the atom in 2D coordinate system.
 */
230
class Atom {
Lukas Pravda's avatar
Lukas Pravda committed
231
    name: string;
Lukas Pravda's avatar
Lukas Pravda committed
232
    labels: any;
Lukas Pravda's avatar
Lukas Pravda committed
233
    position: Vector2D;
Lukas Pravda's avatar
Lukas Pravda committed
234
235
    constructor(item: any) {
        this.name = item.name;
Lukas Pravda's avatar
Lukas Pravda committed
236
        this.labels = item.labels;
Lukas Pravda's avatar
Lukas Pravda committed
237
        this.position = new Vector2D(item.x, item.y)
238
239
    }

Lukas Pravda's avatar
Lukas Pravda committed
240
241
242
243
244
245
246
    /**
     * 
     *
     * @param {Atom} other
     * @returns true if the atoms are equal
     * @memberof Atom
     */
247
248
249
250
    public equals(other: Atom) {
        if (!(other instanceof Atom)) return false;

        return other.name === this.name;
Lukas Pravda's avatar
Lukas Pravda committed
251
252
253
    }
}

Lukas Pravda's avatar
Lukas Pravda committed
254
/**
Lukas Pravda's avatar
Lukas Pravda committed
255
 * 2D point definition
Lukas Pravda's avatar
Lukas Pravda committed
256
257
258
259
260
261
 *
 * @class Point
 * @param {number} x coordinate
 * @param {number} y coordinate
 */
class Vector2D {
Lukas Pravda's avatar
Lukas Pravda committed
262
263
264
    x: number;
    y: number;

Lukas Pravda's avatar
Lukas Pravda committed
265
266

    constructor(x: number, y: number) {
Lukas Pravda's avatar
Lukas Pravda committed
267
        this.x = x;
Lukas Pravda's avatar
Lukas Pravda committed
268
        this.y = y;
Lukas Pravda's avatar
Lukas Pravda committed
269
270
    }

Lukas Pravda's avatar
Lukas Pravda committed
271
272
273
274
275
276
277
    /**
     * 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
278
        return `[${this.x}, ${this.y}]`;
Lukas Pravda's avatar
Lukas Pravda committed
279
280
    }

Lukas Pravda's avatar
Lukas Pravda committed
281
    /**
Lukas Pravda's avatar
Lukas Pravda committed
282
     * Checks whether or not two Vector2D objects are equal.
Lukas Pravda's avatar
Lukas Pravda committed
283
284
285
286
287
288
289
     *
     * @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
290
291
292
293

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

Lukas Pravda's avatar
Lukas Pravda committed
294
295
296
297
298
299
300
301
302

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

Lukas Pravda's avatar
Lukas Pravda committed
306
307
308
309
310
311
312
313
314
315
316

    /**
     * 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
317
318
319
        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
320
        return new Vector2D(x, y);
Lukas Pravda's avatar
Lukas Pravda committed
321
322
323
    }
}

Lukas Pravda's avatar
Lukas Pravda committed
324

Lukas Pravda's avatar
Lukas Pravda committed
325
326
327
328
/**
 * Represents a bond in a 2D depiction.
 *
 * @class Bond
Lukas Pravda's avatar
Lukas Pravda committed
329
330
331
332
 * @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
333
 */
334
335
336
class Bond {
    bgn: Atom;
    end: Atom;
Lukas Pravda's avatar
Lukas Pravda committed
337
338
339
    coords: string;
    style: string;

340

Lukas Pravda's avatar
Lukas Pravda committed
341
342
343
344
345
346
    /**
     *Creates an instance of the bond.
     * @param {Atom} a
     * @param {Atom} b
     * @memberof Bond
     */
Lukas Pravda's avatar
Lukas Pravda committed
347
    constructor(a: Atom, b: Atom, coords: string, style: string) {
348
349
        this.bgn = a;
        this.end = b;
Lukas Pravda's avatar
Lukas Pravda committed
350
351
        this.coords = coords;
        this.style = style.replace("stroke-width:2px", "stroke-width:4px");;
352
353
    }

Lukas Pravda's avatar
Lukas Pravda committed
354
355
356
357
358
359
360
361
362

    /**
     * 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
363
    public getOtherAtom(other: Atom): Atom {
364
        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
365

366
367
368
        return this.bgn.equals(other) ? this.end : this.bgn;
    }

Lukas Pravda's avatar
Lukas Pravda committed
369
370
371
372
373
374
375
376

    /**
     * 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
377
    public containsAtom(other: Atom) {
378
379
380
        return this.bgn.equals(other) || this.end.equals(other);
    }

Lukas Pravda's avatar
Lukas Pravda committed
381
382
383
384
385
386
387
    /**
     * Hide bond from the representation.
     *
     * @memberof Bond
     */
    public hide() {
        this.style.replace("stroke-width:4px", "stroke-width:0px");
388
    }
Lukas Pravda's avatar
Lukas Pravda committed
389
}