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