Completed
Pull Request — develop (#75)
by
unknown
01:04
created

forcegraph.js ➔ ... ➔ d3.distance   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
c 0
b 0
f 0
nc 2
dl 0
loc 6
rs 9.4285
nop 1
1
define(['d3', 'helper'], function (d3, helper) {
2
  'use strict';
3
4
  var margin = 200;
5
  var NODE_RADIUS = 15;
6
  var LINE_RADIUS = 12;
7
8
  return function (config, linkScale, sidebar, router) {
9
    var self = this;
10
    var canvas;
11
    var ctx;
12
    var screenRect;
13
    var nodesDict;
14
    var linksDict;
15
    var zoomBehavior;
16
    var force;
17
    var el;
18
    var font;
19
    var doAnimation = false;
20
    var intNodes = [];
21
    var intLinks = [];
22
    var highlight;
23
    var highlightedNodes = [];
24
    var highlightedLinks = [];
25
    var nodes = [];
26
    var uplinkNodes = [];
27
    var nonUplinkNodes = [];
28
    var unseenNodes = [];
29
    var unknownNodes = [];
30
    var savedPanZoom;
31
32
    var draggedNode;
33
34
    var LINK_DISTANCE = 70;
35
36
    function graphDiameter(n) {
37
      return Math.sqrt(n.length / Math.PI) * LINK_DISTANCE * 1.41;
38
    }
39
40
    function savePositions() {
41
      var save = intNodes.map(function (d) {
42
        return { id: d.o.id, x: d.x, y: d.y };
43
      });
44
45
      localStorage.setItem('graph/nodeposition', JSON.stringify(save));
46
    }
47
48
    function nodeName(d) {
49
      if (d.o.node && d.o.node.nodeinfo) {
50
        return d.o.node.nodeinfo.hostname;
51
      }
52
      return d.o.id;
53
    }
54
55
    function dragstart() {
56
      var e = translateXY(d3.mouse(el));
57
58
      var n = intNodes.filter(function (d) {
59
        return distancePoint(e, d) < NODE_RADIUS;
60
      });
61
62
      if (n.length === 0) {
63
        return;
64
      }
65
66
      draggedNode = n[0];
67
      d3.event.sourceEvent.stopPropagation();
68
      d3.event.sourceEvent.preventDefault();
69
      draggedNode.fixed |= 2;
70
71
      draggedNode.px = draggedNode.x;
72
      draggedNode.py = draggedNode.y;
73
    }
74
75
    function dragmove() {
76
      if (draggedNode) {
77
        var e = translateXY(d3.mouse(el));
78
79
        draggedNode.px = e.x;
80
        draggedNode.py = e.y;
81
        force.resume();
82
      }
83
    }
84
85
    function dragend() {
86
      if (draggedNode) {
87
        d3.event.sourceEvent.stopPropagation();
88
        d3.event.sourceEvent.preventDefault();
89
        draggedNode.fixed &= ~2;
90
        draggedNode = undefined;
91
      }
92
    }
93
94
    var draggableNode = d3.drag()
95
      .on('start', dragstart)
96
      .on('drag', dragmove)
97
      .on('end', dragend);
98
99
    function animatePanzoom(translate, scale) {
100
      var translateP = zoomBehavior.translateExtent();
101
      var scaleP = zoomBehavior.scaleExtent();
102
103
      if (!doAnimation) {
104
        zoomBehavior.translateExtent(translate);
105
        zoomBehavior.scaleExtent(scale);
106
        panzoom();
107
      } else {
108
        var start = { x: translateP[0], y: translateP[1], scale: scaleP };
109
        var end = { x: translate[0], y: translate[1], scale: scale };
110
111
        var interpolate = d3.interpolateObject(start, end);
112
        var duration = 500;
113
114
        var ease = d3.ease('cubic-in-out');
115
116
        d3.timer(function (t) {
117
          if (t >= duration) {
118
            return true;
119
          }
120
121
          var v = interpolate(ease(t / duration));
122
          zoomBehavior.translateExtent([v.x, v.y]);
123
          zoomBehavior.scale(v.scale);
124
          panzoom();
125
126
          return false;
127
        });
128
      }
129
    }
130
131
    function onPanZoom() {
132
      savedPanZoom = {
133
        translate: zoomBehavior.translateExtent(),
134
        scale: zoomBehavior.scale()
135
      };
136
      panzoom();
137
    }
138
139
    function panzoom() {
140
      var translate = zoomBehavior.translateExtent();
141
      var scale = zoomBehavior.scale();
142
143
      panzoomReal(translate, scale);
144
    }
145
    function panzoomReal(translate, scale) {
146
      screenRect = {
147
        left: -translate[0] / scale, top: -translate[1] / scale,
148
        right: (canvas.width - translate[0]) / scale,
149
        bottom: (canvas.height - translate[1]) / scale
150
      };
151
152
      requestAnimationFrame(redraw);
153
    }
154
155
    function getSize() {
156
      var sidebarWidth = sidebar();
157
      var width = el.offsetWidth - sidebarWidth;
158
      var height = el.offsetHeight;
159
160
      return [width, height];
161
    }
162
163
    function panzoomTo(a, b) {
164
      var sidebarWidth = sidebar();
165
      var size = getSize();
166
167
      var targetWidth = Math.max(1, b[0] - a[0]);
168
      var targetHeight = Math.max(1, b[1] - a[1]);
169
170
      var scaleX = size[0] / targetWidth;
171
      var scaleY = size[1] / targetHeight;
172
      var scaleMax = zoomBehavior.scaleExtent()[1];
173
      var scale = 0.5 * Math.min(scaleMax, Math.min(scaleX, scaleY));
174
175
      var centroid = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
176
      var x = -centroid[0] * scale + size[0] / 2;
177
      var y = -centroid[1] * scale + size[1] / 2;
178
      var translate = [x + sidebarWidth, y];
179
180
      animatePanzoom(translate, scale);
181
    }
182
183
    function updateHighlight(nopanzoom) {
184
      highlightedNodes = [];
185
      highlightedLinks = [];
186
187
      if (highlight !== undefined) {
188
        if (highlight.type === 'node') {
189
          var n = nodesDict[highlight.o.nodeinfo.node_id];
190
191
          if (n) {
192
            highlightedNodes = [n];
193
194
            if (!nopanzoom) {
195
              panzoomTo([n.x, n.y], [n.x, n.y]);
196
            }
197
          }
198
199
          return;
200
        } else if (highlight.type === 'link') {
201
          var l = linksDict[highlight.o.id];
202
203
          if (l) {
204
            highlightedLinks = [l];
205
206
            if (!nopanzoom) {
207
              var x = d3.extent([l.source, l.target], function (d) {
208
                return d.x;
209
              });
210
              var y = d3.extent([l.source, l.target], function (d) {
211
                return d.y;
212
              });
213
              panzoomTo([x[0], y[0]], [x[1], y[1]]);
214
            }
215
          }
216
217
          return;
218
        }
219
      }
220
221
      if (!nopanzoom) {
222
        if (!savedPanZoom) {
223
          panzoomTo([0, 0], force.size());
224
        } else {
225
          animatePanzoom(savedPanZoom.translate, savedPanZoom.scale);
226
        }
227
      }
228
    }
229
230
    function drawLabel(d) {
231
      var neighbours = d.neighbours.filter(function (n) {
232
        return n.link.o.type !== 'fastd' && n.link.o.type !== 'L2TP';
233
      });
234
235
      var sum = neighbours.reduce(function (a, b) {
236
        return [a[0] + b.node.x, a[1] + b.node.y];
237
      }, [0, 0]);
238
239
      var sumCos = sum[0] - d.x * neighbours.length;
240
      var sumSin = sum[1] - d.y * neighbours.length;
241
242
      var angle = Math.PI / 2;
243
244
      if (neighbours.length > 0) {
245
        angle = Math.PI + Math.atan2(sumSin, sumCos);
246
      }
247
248
      var cos = Math.cos(angle);
249
      var sin = Math.sin(angle);
250
251
      var width = d.labelWidth;
252
      var height = d.labelHeight;
253
254
      var x = d.x + d.labelA * Math.pow(Math.abs(cos), 2 / 5) * Math.sign(cos) - width / 2;
255
      var y = d.y + d.labelB * Math.pow(Math.abs(sin), 2 / 5) * Math.sign(sin) - height / 2;
256
257
      ctx.drawImage(d.label, x, y, width, height);
258
    }
259
260
    function visibleLinks(d) {
261
      return (d.source.x > screenRect.left && d.source.x < screenRect.right &&
262
        d.source.y > screenRect.top && d.source.y < screenRect.bottom) ||
263
        (d.target.x > screenRect.left && d.target.x < screenRect.right &&
264
        d.target.y > screenRect.top && d.target.y < screenRect.bottom);
265
    }
266
267
    function visibleNodes(d) {
268
      return d.x + margin > screenRect.left && d.x - margin < screenRect.right &&
269
        d.y + margin > screenRect.top && d.y - margin < screenRect.bottom;
270
    }
271
272
    function drawNode(color, radius, scale, r) {
273
      var node = document.createElement('canvas');
274
      node.height = node.width = scale * radius * 8 * r;
275
276
      var nctx = node.getContext('2d');
277
      nctx.scale(scale * r, scale * r);
278
      nctx.save();
279
280
      nctx.translate(-node.width / scale, -node.height / scale);
281
      nctx.lineWidth = radius;
282
283
      nctx.beginPath();
284
      nctx.moveTo(radius, 0);
285
      nctx.arc(0, 0, radius, 0, 2 * Math.PI);
286
287
      nctx.restore();
288
      nctx.translate(node.width / 2 / scale / r, node.height / 2 / scale / r);
289
290
      nctx.beginPath();
291
      nctx.moveTo(radius, 0);
292
      nctx.arc(0, 0, radius, 0, 2 * Math.PI);
293
294
      nctx.strokeStyle = color;
295
      nctx.lineWidth = radius;
296
      nctx.stroke();
297
298
      return node;
299
    }
300
301
    function redraw() {
302
      var r = window.devicePixelRatio;
303
      var translate = zoomBehavior.translateExtent();
304
      var scale = zoomBehavior.scale();
305
      var links = intLinks.filter(visibleLinks);
306
307
      ctx.save();
308
      ctx.setTransform(1, 0, 0, 1, 0, 0);
309
      ctx.clearRect(0, 0, canvas.width, canvas.height);
310
      ctx.restore();
311
312
      ctx.save();
313
      ctx.translate(translate[0], translate[1]);
314
      ctx.scale(scale, scale);
315
316
      var clientColor = 'rgba(230, 50, 75, 1.0)';
317
      var unknownColor = '#D10E2A';
318
      var nonUplinkColor = '#F2E3C6';
319
      var uplinkColor = '#5BAAEB';
320
      var unseenColor = '#FFA726';
321
      var highlightColor = 'rgba(252, 227, 198, 0.15)';
322
      var nodeRadius = 6;
323
      var cableColor = '#50B0F0';
324
325
      // -- draw links --
326
      ctx.save();
327
      links.forEach(function (d) {
328
        var dx = d.target.x - d.source.x;
329
        var dy = d.target.y - d.source.y;
330
        var a = Math.sqrt(dx * dx + dy * dy);
331
        dx /= a;
332
        dy /= a;
333
334
        ctx.beginPath();
335
        ctx.moveTo(d.source.x + dx * nodeRadius, d.source.y + dy * nodeRadius);
336
        ctx.lineTo(d.target.x - dx * nodeRadius, d.target.y - dy * nodeRadius);
337
        ctx.strokeStyle = d.o.type === 'Kabel' ? cableColor : d.color;
338
        ctx.globalAlpha = d.o.type === 'fastd' || d.o.type === 'L2TP' ? 0.2 : 0.8;
339
        ctx.lineWidth = d.o.type === 'fastd' || d.o.type === 'L2TP' ? 1.5 : 2.5;
340
        ctx.stroke();
341
      });
342
343
      ctx.restore();
344
345
      // -- draw unknown nodes --
346
      ctx.beginPath();
347
      unknownNodes.filter(visibleNodes).forEach(function (d) {
348
        ctx.moveTo(d.x + nodeRadius, d.y);
349
        ctx.arc(d.x, d.y, nodeRadius, 0, 2 * Math.PI);
350
      });
351
352
      ctx.strokeStyle = unknownColor;
353
      ctx.lineWidth = nodeRadius;
354
355
      ctx.stroke();
356
357
      // -- draw nodes --
358
      ctx.save();
359
      ctx.scale(1 / scale / r, 1 / scale / r);
360
361
      var nonUplinkNode = drawNode(nonUplinkColor, nodeRadius, scale, r);
362
      nonUplinkNodes.filter(visibleNodes).forEach(function (d) {
363
        ctx.drawImage(nonUplinkNode, scale * r * d.x - nonUplinkNode.width / 2, scale * r * d.y - nonUplinkNode.height / 2);
364
      });
365
366
      var uplinkNode = drawNode(uplinkColor, nodeRadius, scale, r);
367
      uplinkNodes.filter(visibleNodes).forEach(function (d) {
368
        ctx.drawImage(uplinkNode, scale * r * d.x - uplinkNode.width / 2, scale * r * d.y - uplinkNode.height / 2);
369
      });
370
371
      var unseenNode = drawNode(unseenColor, nodeRadius, scale, r);
372
      unseenNodes.filter(visibleNodes).forEach(function (d) {
373
        ctx.drawImage(unseenNode, scale * r * d.x - unseenNode.width / 2, scale * r * d.y - unseenNode.height / 2);
374
      });
375
376
      ctx.restore();
377
378
      // -- draw clients --
379
      ctx.save();
380
      ctx.beginPath();
381
      if (scale > 0.9) {
382
        var startDistance = 16;
383
        nodes.filter(visibleNodes).forEach(function (d) {
384
          helper.positionClients(ctx, d, Math.PI, d.o.node.statistics.clients, startDistance);
385
        });
386
      }
387
      ctx.fillStyle = clientColor;
388
      ctx.fill();
389
      ctx.restore();
390
391
      // -- draw node highlights --
392
      if (highlightedNodes.length) {
393
        ctx.save();
394
        ctx.globalCompositeOperation = 'lighten';
395
        ctx.fillStyle = highlightColor;
396
397
        ctx.beginPath();
398
        highlightedNodes.forEach(function (d) {
399
          ctx.moveTo(d.x + 5 * nodeRadius, d.y);
400
          ctx.arc(d.x, d.y, 5 * nodeRadius, 0, 2 * Math.PI);
401
        });
402
        ctx.fill();
403
404
        ctx.restore();
405
      }
406
407
      // -- draw link highlights --
408
      if (highlightedLinks.length) {
409
        ctx.save();
410
        ctx.lineWidth = 2 * 5 * nodeRadius;
411
        ctx.globalCompositeOperation = 'lighten';
412
        ctx.strokeStyle = highlightColor;
413
        ctx.lineCap = 'round';
414
415
        ctx.beginPath();
416
        highlightedLinks.forEach(function (d) {
417
          ctx.moveTo(d.source.x, d.source.y);
418
          ctx.lineTo(d.target.x, d.target.y);
419
        });
420
        ctx.stroke();
421
422
        ctx.restore();
423
      }
424
425
      // -- draw labels --
426
      if (scale > 0.9) {
427
        intNodes.filter(visibleNodes).forEach(drawLabel, scale);
428
      }
429
430
      ctx.restore();
431
    }
432
433
    function tickEvent() {
434
      redraw();
435
    }
436
437
    function resizeCanvas() {
438
      var r = window.devicePixelRatio;
439
      canvas.width = el.offsetWidth * r;
440
      canvas.height = el.offsetHeight * r;
441
      canvas.style.width = el.offsetWidth + 'px';
442
      canvas.style.height = el.offsetHeight + 'px';
443
      ctx.setTransform(1, 0, 0, 1, 0, 0);
444
      ctx.scale(r, r);
445
      requestAnimationFrame(redraw);
446
    }
447
448
    function distance(a, b) {
449
      return Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2);
450
    }
451
452
    function distancePoint(a, b) {
453
      return Math.sqrt(distance(a, b));
454
    }
455
456
    function distanceLink(p, a, b) {
457
      /* http://stackoverflow.com/questions/849211 */
458
459
      var l2 = distance(a, b);
460
461
      if (l2 === 0) {
462
        return distance(p, a);
463
      }
464
465
      var t = ((p.x - a.x) * (b.x - a.x) + (p.y - a.y) * (b.y - a.y)) / l2;
466
467
      if (t < 0) {
468
        return distance(p, a);
469
      }
470
471
      if (t > 1) {
472
        return distance(p, b);
473
      }
474
475
      return Math.sqrt(distance(p, {
476
        x: a.x + t * (b.x - a.x),
477
        y: a.y + t * (b.y - a.y)
478
      }));
479
    }
480
481
    function translateXY(d) {
482
      var translate = zoomBehavior.translateExtent();
483
      var scale = zoomBehavior.scale();
484
485
      return {
486
        x: (d[0] - translate[0]) / scale,
487
        y: (d[1] - translate[1]) / scale
488
      };
489
    }
490
491
    function onClick() {
492
      if (d3.event.defaultPrevented) {
493
        return;
494
      }
495
496
      var e = translateXY(d3.mouse(el));
497
498
      var n = intNodes.filter(function (d) {
499
        return distancePoint(e, d) < NODE_RADIUS;
500
      });
501
502
      if (n.length > 0) {
503
        router.node(n[0].o.node)();
504
        return;
505
      }
506
507
      var links = intLinks.filter(function (d) {
508
        return d.o.type !== 'fastd' && d.o.type !== 'L2TP';
509
      }).filter(function (d) {
510
        return distanceLink(e, d.source, d.target) < LINE_RADIUS;
511
      });
512
513
      if (links.length > 0) {
514
        router.link(links[0].o)();
515
      }
516
    }
517
518
    function zoom(z, scale) {
519
      var size = getSize();
520
      var newSize = [size[0] / scale, size[1] / scale];
521
522
      var sidebarWidth = sidebar();
523
      var delta = [size[0] - newSize[0], size[1] - newSize[1]];
524
      var translate = z.translate();
525
      var translateNew = [sidebarWidth + (translate[0] - sidebarWidth - delta[0] / 2) * scale, (translate[1] - delta[1] / 2) * scale];
526
527
      animatePanzoom(translateNew, z.scale() * scale);
528
    }
529
530
    function keyboardZoom(z) {
531
      return function () {
532
        var e = d3.event;
533
534
        if (e.altKey || e.ctrlKey || e.metaKey) {
535
          return;
536
        }
537
538
        if (e.keyCode === 43) {
539
          zoom(z, 1.41);
540
        }
541
542
        if (e.keyCode === 45) {
543
          zoom(z, 1 / 1.41);
544
        }
545
      };
546
    }
547
548
    el = document.createElement('div');
549
    el.classList.add('graph');
550
551
    font = window.getComputedStyle(el).fontSize + ' ' + window.getComputedStyle(el).fontFamily;
552
553
    zoomBehavior = d3.zoom()
554
      .scaleExtent([1 / 3, 3])
555
      .on('zoom', onPanZoom)
556
      .translateExtent([sidebar(), 0]);
557
558
    canvas = d3.select(el)
559
      .attr('tabindex', 1)
560
      .on('keypress', keyboardZoom(zoomBehavior))
561
      .call(zoomBehavior)
562
      .append('canvas')
563
      .on('click', onClick)
564
      .call(draggableNode)
565
      .node();
566
567
    ctx = canvas.getContext('2d');
568
569
    force = d3.forceSimulation()
570
      .force('charge', -250)
571
      .force('gravity', function (d) {
572
        d.x += (d.cx - d.x) * 0.1;
573
        d.y += (d.cy - d.y) * 0.1;
574
      })
575
      .force('link', d3.forceLink()
576
        .distance(function (d) {
577
          if (d.o.type === 'fastd' || d.o.type === 'L2TP') {
578
            return 0;
579
          }
580
          return LINK_DISTANCE;
581
        })
582
        .strength(function (d) {
583
          if (d.o.type === 'fastd' || d.o.type === 'L2TP') {
584
            return 0.02;
585
          }
586
          return Math.max(0.5, 1 / d.o.tq);
587
        })
588
      )
589
      .on('tick', tickEvent)
590
      .on('end', savePositions);
591
592
    window.addEventListener('resize', resizeCanvas);
593
594
    panzoom();
595
596
    self.setData = function setData(data) {
597
      var oldNodes = {};
598
599
      intNodes.forEach(function (d) {
600
        oldNodes[d.o.id] = d;
601
      });
602
603
      intNodes = data.graph.nodes.map(function (d) {
604
        var e;
605
        if (d.id in oldNodes) {
606
          e = oldNodes[d.id];
607
        } else {
608
          e = {};
609
        }
610
611
        e.o = d;
612
613
        return e;
614
      });
615
616
      var newNodesDict = {};
617
618
      intNodes.forEach(function (d) {
619
        newNodesDict[d.o.id] = d;
620
      });
621
622
      var oldLinks = {};
623
624
      intLinks.forEach(function (d) {
625
        oldLinks[d.o.id] = d;
626
      });
627
628
      intLinks = data.graph.links.map(function (d) {
629
        var e;
630
        if (d.id in oldLinks) {
631
          e = oldLinks[d.id];
632
        } else {
633
          e = {};
634
        }
635
636
        e.o = d;
637
        e.source = newNodesDict[d.source.id];
638
        e.target = newNodesDict[d.target.id];
639
        e.color = linkScale(d.tq).hex();
640
641
        return e;
642
      });
643
644
      linksDict = {};
645
      nodesDict = {};
646
647
      intNodes.forEach(function (d) {
648
        d.neighbours = {};
649
650
        if (d.o.node) {
651
          nodesDict[d.o.node.nodeinfo.node_id] = d;
652
        }
653
654
        var name = nodeName(d);
655
656
        var offset = 5;
657
        var lineWidth = 3;
658
        var buffer = document.createElement('canvas');
659
        var r = window.devicePixelRatio;
660
        var bctx = buffer.getContext('2d');
661
        bctx.font = font;
662
        var width = bctx.measureText(name).width;
663
        var scale = zoomBehavior.scaleExtent()[1] * r;
664
        buffer.width = (width + 2 * lineWidth) * scale;
665
        buffer.height = (16 + 2 * lineWidth) * scale;
666
        bctx.font = font;
667
        bctx.scale(scale, scale);
668
        bctx.textBaseline = 'middle';
669
        bctx.textAlign = 'center';
670
        bctx.fillStyle = 'rgba(242, 227, 198, 1.0)';
671
        bctx.fillText(name, buffer.width / (2 * scale), buffer.height / (2 * scale));
672
673
        d.label = buffer;
674
        d.labelWidth = buffer.width / scale;
675
        d.labelHeight = buffer.height / scale;
676
        d.labelA = offset + buffer.width / (2 * scale);
677
        d.labelB = offset + buffer.height / (2 * scale);
678
      });
679
680
      intNodes.forEach(function (d) {
681
        d.neighbours = Object.keys(d.neighbours).map(function (k) {
682
          return d.neighbours[k];
683
        });
684
      });
685
686
      nodes = intNodes.filter(function (d) {
687
        return !d.o.unseen && d.o.node;
688
      });
689
      uplinkNodes = nodes.filter(function (d) {
690
        return d.o.node.flags.uplink;
691
      });
692
      nonUplinkNodes = nodes.filter(function (d) {
693
        return !d.o.node.flags.uplink;
694
      });
695
      unseenNodes = intNodes.filter(function (d) {
696
        return d.o.unseen && d.o.node;
697
      });
698
      unknownNodes = intNodes.filter(function (d) {
699
        return !d.o.node;
700
      });
701
702
      var save = JSON.parse(localStorage.getItem('graph/nodeposition'));
703
704
      if (save) {
705
        var nodePositions = {};
706
        save.forEach(function (d) {
707
          nodePositions[d.id] = d;
708
        });
709
710
        intNodes.forEach(function (d) {
711
          if (nodePositions[d.o.id] && (d.x === undefined || d.y === undefined)) {
712
            d.x = nodePositions[d.o.id].x;
713
            d.y = nodePositions[d.o.id].y;
714
          }
715
        });
716
      }
717
718
      var diameter = graphDiameter(intNodes);
719
720
      force.nodes(intNodes)
721
        .links(intLinks)
722
        .size([diameter, diameter]);
723
724
      updateHighlight(true);
725
726
      force.start();
727
      resizeCanvas();
728
    };
729
730
    self.resetView = function resetView() {
731
      highlight = undefined;
732
      updateHighlight();
733
      doAnimation = true;
734
    };
735
736
    self.gotoNode = function gotoNode(d) {
737
      highlight = { type: 'node', o: d };
738
      updateHighlight();
739
      doAnimation = true;
740
    };
741
742
    self.gotoLink = function gotoLink(d) {
743
      highlight = { type: 'link', o: d };
744
      updateHighlight();
745
      doAnimation = true;
746
    };
747
748
    self.destroy = function destroy() {
749
      force.stop();
750
      canvas.remove();
751
      force = null;
752
753
      if (el.parentNode) {
754
        el.parentNode.removeChild(el);
755
      }
756
    };
757
758
    self.render = function render(d) {
759
      d.appendChild(el);
760
      resizeCanvas();
761
      updateHighlight();
762
    };
763
764
    return self;
765
  };
766
});
767