From 4b076ab3b9a6c87d22c00f2d1cb7354bd3d8561b Mon Sep 17 00:00:00 2001
From: Nils Schneider <nils@nilsschneider.net>
Date: Tue, 31 Mar 2015 17:22:36 +0200
Subject: [PATCH] add forcegraph

---
 app.js                |   1 +
 bower.json            |   3 +-
 img/geometry2.png     | Bin 0 -> 2138 bytes
 lib/forcegraph.js     | 211 ++++++++++++++++++++++++++++++++++++++++++
 scss/_forcegraph.scss |  57 ++++++++++++
 scss/main.scss        |   1 +
 tasks/build.js        |   5 +
 7 files changed, 277 insertions(+), 1 deletion(-)
 create mode 100644 img/geometry2.png
 create mode 100644 lib/forcegraph.js
 create mode 100644 scss/_forcegraph.scss

diff --git a/app.js b/app.js
index 88f666b..d5e5602 100644
--- a/app.js
+++ b/app.js
@@ -7,6 +7,7 @@ require.config({
     "moment": "../bower_components/moment/min/moment-with-locales.min",
     "tablesort": "../bower_components/tablesort/tablesort.min",
     "tablesort.numeric": "../bower_components/tablesort/src/sorts/tablesort.numeric",
+    "d3": "../bower_components/d3/d3.min",
     "helper": "../helper"
   },
   shim: {
diff --git a/bower.json b/bower.json
index 0c3b47e..01cbcd2 100644
--- a/bower.json
+++ b/bower.json
@@ -19,7 +19,8 @@
     "roboto-slab-fontface": "*",
     "es6-shim": "~0.27.1",
     "almond": "~0.3.1",
-    "r.js": "~2.1.16"
+    "r.js": "~2.1.16",
+    "d3": "~3.5.5"
   },
   "authors": [
     "Nils Schneider <nils@nilsschneider.net>"
diff --git a/img/geometry2.png b/img/geometry2.png
new file mode 100644
index 0000000000000000000000000000000000000000..d43966e05f943614a9b25746a660cf156f1a2f66
GIT binary patch
literal 2138
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

literal 0
HcmV?d00001

diff --git a/lib/forcegraph.js b/lib/forcegraph.js
new file mode 100644
index 0000000..681ecec
--- /dev/null
+++ b/lib/forcegraph.js
@@ -0,0 +1,211 @@
+// TODO
+// - window size
+// - avoid sidebar
+// - pan to node
+// - pan and zoom to link
+define(["d3"], function (d3) {
+   return function (linkScale, sidebar, router) {
+    var self = this
+    var vis, link, node, label
+    var nodesDict, linksDict
+    var force
+
+    function nodeName(d) {
+      if (d.node && d.node.nodeinfo)
+        return d.node.nodeinfo.hostname
+      else
+        return d.id
+    }
+
+    function dragstart(d) {
+      d3.event.sourceEvent.stopPropagation()
+      d.fixed |= 2
+    }
+
+    function dragmove(d) {
+      d.px = d3.event.x
+      d.py = d3.event.y
+      force.resume()
+    }
+
+    function dragend(d) {
+      d3.event.sourceEvent.stopPropagation()
+      d.fixed &= 1
+    }
+
+    function panzoom() {
+      vis.attr("transform",
+          "translate(" + d3.event.translate + ") "
+          + "scale(" + d3.event.scale + ")")
+    }
+
+    function tickEvent() {
+      link.selectAll("line")
+          .attr("x1", function(d) { return d.source.x })
+          .attr("y1", function(d) { return d.source.y })
+          .attr("x2", function(d) { return d.target.x })
+          .attr("y2", function(d) { return d.target.y })
+
+      node
+         .attr("cx", function(d) { return d.x })
+         .attr("cy", function(d) { return d.y })
+
+      label.attr("transform", function(d) {
+        return "translate(" + d.x + "," + d.y + ")"
+      })
+    }
+
+    var el = document.createElement("div")
+    el.classList.add("graph")
+    self.div = el
+
+    vis = d3.select(el).append("svg")
+                .attr("pointer-events", "all")
+                .call(d3.behavior.zoom().on("zoom", panzoom))
+                .append("g")
+
+    vis.append("g").attr("class", "links")
+    vis.append("g").attr("class", "nodes")
+    vis.append("g").attr("class", "labels").attr("pointer-events", "none")
+
+    force = d3.layout.force()
+              .size([500, 500])
+              .charge(-100)
+              .gravity(0.05)
+              .friction(0.73)
+              .theta(0.8)
+              .linkDistance(70)
+              .linkStrength(0.2)
+              .on("tick", tickEvent)
+
+    var draggableNode = d3.behavior.drag()
+                          .on("dragstart", dragstart)
+                          .on("drag", dragmove)
+                          .on("dragend", dragend)
+
+    self.setData = function (data) {
+      var links = data.graph.links.filter( function (d) {
+        return !d.vpn
+      })
+
+      link = vis.select("g.links")
+                    .selectAll("g.link")
+                    .data(links, linkId)
+
+      var linkEnter = link.enter().append("g")
+                          .attr("class", "link")
+                          .on("click", function (d) {
+                            if (!d3.event.defaultPrevented)
+                              router.link(d)()
+                          })
+
+      linkEnter.append("line")
+               .append("title")
+
+      link.selectAll("line")
+          .style("stroke", function (d) { return linkScale(d.tq) })
+
+      link.selectAll("title").text(showTq)
+
+      linksDict = {}
+
+      link.each( function (d) {
+        if (d.source.node && d.target.node)
+          linksDict[linkId(d)] = this
+      })
+
+      var nodes = data.graph.nodes
+
+      node = vis.select("g.nodes")
+                .selectAll(".node")
+                .data(nodes,
+                  function(d) {
+                    return d.id
+                  }
+                )
+
+      var nodeEnter = node.enter().append("circle")
+                          .attr("r", 8)
+                          .on("click", function (d) {
+                            if (!d3.event.defaultPrevented)
+                              router.node(d.node)()
+                          })
+                          .call(draggableNode)
+
+      node.attr("class", function (d) {
+        var s = ["node"]
+        if (!d.node)
+          s.push("unknown")
+
+        return s.join(" ")
+      })
+
+      nodesDict = {}
+
+      node.each( function (d) {
+        if (d.node)
+          nodesDict[d.node.nodeinfo.node_id] = this
+      })
+
+      label = vis.select("g.labels")
+                    .selectAll("g.label")
+                    .data(data.graph.nodes, function (d) {
+                      return d.id
+                    })
+
+      var labelEnter = label.enter()
+                        .append("g")
+                        .attr("class", "label")
+
+      labelEnter.append("path").attr("class", "clients")
+
+      labelEnter.append("text")
+                .attr("class", "name")
+                .attr("text-anchor", "middle")
+                .attr("y", "21px")
+                .attr("x", "0px")
+
+      label.selectAll("text.name").text(nodeName)
+
+      var labelTextWidth = function (e) {
+        return e.parentNode.querySelector("text").getBBox().width + 3
+      }
+
+      labelEnter.insert("rect", "text")
+                .attr("y", "10px")
+                .attr("x", function() { return labelTextWidth(this) / (-2)})
+                .attr("width", function() { return labelTextWidth(this)})
+                .attr("height", "15px")
+
+      nodeEnter.append("title")
+
+      node.selectAll("title").text(nodeName)
+
+      force.nodes(nodes)
+           .links(links)
+           .alpha(0.1)
+           .start()
+    }
+
+    self.resetView = function () {
+      node.classed("highlight", false)
+      link.classed("highlight", false)
+    }
+
+    self.gotoNode = function (d) {
+      link.classed("highlight", false)
+      node.classed("highlight", function (e) {
+        return e.node === d && d !== undefined
+      })
+    }
+
+    self.gotoLink = function (d) {
+      node.classed("highlight", false)
+      link.classed("highlight", function (e) {
+        return e === d && d !== undefined
+      })
+    }
+
+    return self
+  }
+})
diff --git a/scss/_forcegraph.scss b/scss/_forcegraph.scss
new file mode 100644
index 0000000..29e7678
--- /dev/null
+++ b/scss/_forcegraph.scss
@@ -0,0 +1,57 @@
+.graph {
+  height: 100vh;
+  background: url(img/geometry2.png);
+
+  svg {
+    display: block;
+    width: 100%;
+    height: 100%;
+  }
+
+  .link {
+    stroke-opacity: 0.8;
+
+    line {
+      stroke-width: 2.5px;
+      cursor: pointer;
+    }
+
+    &.highlight line {
+      stroke-width: 7px;
+      stroke-dasharray: 10, 10;
+      opacity: 1;
+    }
+  }
+
+  .node {
+    fill: #fff;
+    stroke-width: 2.5px;
+    stroke: #AEC7E8;
+    
+    &:not(.unknown) {
+      cursor: pointer;
+    }
+
+    &.highlight {
+      stroke: #FFD486;
+      stroke-width: 6px;
+      fill: orange;
+      point-order: stroke;
+    }
+
+    &.unknown {
+      stroke: #d00000;
+    }
+  }
+
+  .label {
+    text {
+      font-size: 0.8em;
+    }
+
+    rect {
+      fill: rgba(255, 255, 255, 0.8);
+    }
+  }
+}  
+
diff --git a/scss/main.scss b/scss/main.scss
index 0589d67..178edc8 100644
--- a/scss/main.scss
+++ b/scss/main.scss
@@ -1,5 +1,6 @@
 @import '_leaflet';
 @import '_leaflet.label';
+@import '_forcegraph';
 
 .stroke-first {
   paint-order: stroke;
diff --git a/tasks/build.js b/tasks/build.js
index 1846ec7..cfa89a7 100644
--- a/tasks/build.js
+++ b/tasks/build.js
@@ -7,6 +7,11 @@ module.exports = function(grunt) {
         cwd: "html/",
         dest: "build/"
       },
+      img: {
+        src: ["img/*"],
+        expand: true,
+        dest: "build/"
+      },
       vendorjs: {
         src: [ "es6-shim/es6-shim.min.js",
                "intl/Intl.complete.js"
-- 
GitLab