Browse Source

forcegraph: add labels

Nils Schneider 9 years ago
parent
commit
020ab2aef5
2 changed files with 149 additions and 66 deletions
  1. 140 60
      lib/forcegraph.js
  2. 9 6
      scss/_forcegraph.scss

+ 140 - 60
lib/forcegraph.js

@@ -1,7 +1,7 @@
 define(["d3"], function (d3) {
    return function (config, linkScale, sidebar, router) {
     var self = this
-    var svg, vis, link, node
+    var svg, vis, link, node, label
     var nodesDict, linksDict
     var zoomBehavior
     var force
@@ -14,7 +14,7 @@ define(["d3"], function (d3) {
     var LINK_DISTANCE = 70
 
     function graphDiameter(nodes) {
-      return Math.sqrt(nodes.length / Math.PI) * LINK_DISTANCE
+      return Math.sqrt(nodes.length / Math.PI) * LINK_DISTANCE * 1.41
     }
 
     function savePositions() {
@@ -51,6 +51,11 @@ define(["d3"], function (d3) {
       d.fixed &= 1
     }
 
+    var draggableNode = d3.behavior.drag()
+                          .on("dragstart", dragstart)
+                          .on("drag", dragmove)
+                          .on("dragend", dragend)
+
     function animatePanzoom(translate, scale) {
       zoomBehavior.scale(scale)
       zoomBehavior.translate(translate)
@@ -141,6 +146,112 @@ define(["d3"], function (d3) {
         panzoomTo([0, 0], force.size())
     }
 
+    function updateLinks(vis, data) {
+      var link = vis.selectAll("g.link")
+                .data(data, function (d) { return d.o.id })
+
+      link.exit().remove()
+
+      var linkEnter = link.enter().append("g")
+                          .attr("class", "link")
+                          .on("click", function (d) {
+                            if (!d3.event.defaultPrevented)
+                              router.link(d.o)()
+                          })
+
+      linkEnter.append("line")
+               .append("title")
+
+      link.selectAll("line")
+          .style("stroke", function (d) { return linkScale(d.o.tq).hex() })
+
+      link.selectAll("title").text(function (d) { return showTq(d.o) })
+
+      return link
+    }
+
+    function updateNodes(vis, data) {
+      var node = vis.selectAll(".node")
+                    .data(data, function(d) { return d.o.id })
+
+      node.exit().remove()
+
+      node.enter().append("circle")
+          .attr("r", 8)
+          .on("click", function (d) {
+            if (!d3.event.defaultPrevented)
+              router.node(d.o.node)()
+          })
+          .call(draggableNode)
+
+      node.attr("class", function (d) {
+        var s = ["node"]
+        if (!d.o.node)
+          s.push("unknown")
+
+        return s.join(" ")
+      })
+
+      return node
+    }
+
+    function updateLabels(vis, data) {
+      var label = vis.selectAll("text")
+                     .data(data, function(d) { return d.o.id })
+
+      label.exit().remove()
+
+      var labelEnter = label.enter().append("text")
+
+      label.text(nodeName)
+           .each(function (d) {
+             var bbox = this.getBBox()
+             d.labelHeight = bbox.height
+             d.labelWidth = bbox.width
+           })
+
+      labelEnter.each(function (d) {
+        d.labelAngle = Math.PI / 2
+      })
+
+      return label
+    }
+
+    function positionLabels() {
+      label.attr("transform", function(d) {
+        var neighbours = d.neighbours.map(function (n) {
+          var dx = n.x - d.x
+          var dy = n.y - d.y
+
+          return (2 * Math.PI + Math.atan2(dy, dx)) % (2 * Math.PI)
+        })
+
+        var sumCos = neighbours.reduce(function (a, b) {
+          return a + Math.cos(b)
+        }, 0)
+
+        var sumSin = neighbours.reduce(function (a, b) {
+          return a + Math.sin(b)
+        }, 0)
+
+        if (neighbours.length > 0)
+          d.labelAngle = Math.PI + Math.atan2(sumSin, sumCos)
+
+        var offset = 10
+
+        var a = offset + d.labelWidth / 2
+        var b = offset + d.labelHeight / 2
+
+        var cos = Math.cos(d.labelAngle)
+        var sin = Math.sin(d.labelAngle)
+
+        var x = d.x + a * Math.pow(Math.abs(cos), 2 / 5) * Math.sign(cos)
+        var y = d.y + b * Math.pow(Math.abs(sin), 2 / 5) * Math.sign(sin)
+
+        return "translate(" + x + "," + y + ")"
+      })
+    }
+
     function tickEvent() {
       link.selectAll("line")
           .attr("x1", function(d) { return d.source.x })
@@ -148,9 +259,9 @@ define(["d3"], function (d3) {
           .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 })
+      node.attr("cx", function (d) { return d.x })
+                         .attr("cy", function (d) { return d.y })
+      positionLabels()
     }
 
     el = document.createElement("div")
@@ -168,26 +279,23 @@ define(["d3"], function (d3) {
 
     vis = svg.append("g")
 
-    vis.append("g").attr("class", "links")
-    vis.append("g").attr("class", "nodes")
+    var visLinks = vis.append("g").attr("class", "links")
+    var visLabels = vis.append("g").attr("class", "labels")
+    var visNodes = vis.append("g").attr("class", "nodes")
 
     force = d3.layout.force()
-              .charge(-70)
-              .gravity(0.05)
+              .charge(-80)
+              .gravity(0.01)
+              .chargeDistance(8 * LINK_DISTANCE)
               .linkDistance(LINK_DISTANCE)
               .linkStrength(function (d) {
-                return 1 / d.o.tq
+                return Math.max(0.5, 1 / d.o.tq)
               })
               .on("tick", tickEvent)
               .on("end", savePositions)
 
     panzoom()
 
-    var draggableNode = d3.behavior.drag()
-                          .on("dragstart", dragstart)
-                          .on("drag", dragmove)
-                          .on("dragend", dragend)
-
     self.setData = function (data) {
       var oldNodes = {}
 
@@ -235,26 +343,24 @@ define(["d3"], function (d3) {
         return e
       })
 
-      link = vis.select("g.links")
-                .selectAll("g.link")
-                .data(intLinks, function (d) { return d.o.id })
-
-      link.exit().remove()
-
-      var linkEnter = link.enter().append("g")
-                          .attr("class", "link")
-                          .on("click", function (d) {
-                            if (!d3.event.defaultPrevented)
-                              router.link(d.o)()
-                          })
+      intNodes.forEach(function (d) {
+        d.neighbours = {}
+      })
 
-      linkEnter.append("line")
-               .append("title")
+      intLinks.forEach(function (d) {
+        d.source.neighbours[d.target.o.id] = d.target
+        d.target.neighbours[d.source.o.id] = d.source
+      })
 
-      link.selectAll("line")
-          .style("stroke", function (d) { return linkScale(d.o.tq).hex() })
+      intNodes.forEach(function (d) {
+        d.neighbours = Object.keys(d.neighbours).map(function (k) {
+          return d.neighbours[k]
+        })
+      })
 
-      link.selectAll("title").text(function (d) { return showTq(d.o) })
+      link = updateLinks(visLinks, intLinks)
+      node = updateNodes(visNodes, intNodes)
+      label = updateLabels(visLabels, intNodes)
 
       linksDict = {}
 
@@ -263,28 +369,6 @@ define(["d3"], function (d3) {
           linksDict[d.o.id] = d
       })
 
-      node = vis.select("g.nodes")
-                .selectAll(".node")
-                .data(intNodes, function(d) { return d.o.id })
-
-      node.exit().remove()
-
-      var nodeEnter = node.enter().append("circle")
-                          .attr("r", 8)
-                          .on("click", function (d) {
-                            if (!d3.event.defaultPrevented)
-                              router.node(d.o.node)()
-                          })
-                          .call(draggableNode)
-
-      node.attr("class", function (d) {
-        var s = ["node"]
-        if (!d.o.node)
-          s.push("unknown")
-
-        return s.join(" ")
-      })
-
       nodesDict = {}
 
       node.each( function (d) {
@@ -292,8 +376,6 @@ define(["d3"], function (d3) {
           nodesDict[d.o.node.nodeinfo.node_id] = d
       })
 
-      nodeEnter.append("title")
-
       if (localStorageTest()) {
         var save = JSON.parse(localStorage.getItem("graph/nodeposition"))
 
@@ -303,8 +385,8 @@ define(["d3"], function (d3) {
             nodePositions[d.id] = d
           })
 
-          nodeEnter.each( function (d) {
-            if (nodePositions[d.o.id]) {
+          node.each( function (d) {
+            if (nodePositions[d.o.id] && (d.x === undefined || d.y === undefined)) {
               d.x = nodePositions[d.o.id].x
               d.y = nodePositions[d.o.id].y
             }
@@ -312,8 +394,6 @@ define(["d3"], function (d3) {
         }
       }
 
-      node.selectAll("title").text(nodeName)
-
       var diameter = graphDiameter(intNodes)
 
       force.nodes(intNodes)

+ 9 - 6
scss/_forcegraph.scss

@@ -45,13 +45,16 @@
     }
   }
 
-  .label {
+  .labels {
     text {
-      font-size: 0.8em;
-    }
-
-    rect {
-      fill: rgba(255, 255, 255, 0.8);
+      font-size: 7pt;
+      font-family: Roboto;
+      alignment-baseline: central;
+      text-anchor: middle;
+      fill: rgba(0, 0, 0, 0.6);
+      paint-order: stroke;
+      stroke-width: 3pt;
+      stroke: rgba(255, 255, 255, 0.5);
     }
   }
 }