HTRACE-142. Details page: Update graph
diff --git a/htrace-core/src/web/app/setup.js b/htrace-core/src/web/app/setup.js
index 3c32118..adb100c 100644
--- a/htrace-core/src/web/app/setup.js
+++ b/htrace-core/src/web/app/setup.js
@@ -45,76 +45,77 @@
   },
 
   "search": function(query) {
-    var top = new app.SearchView();
-    app.root.app.show(top);
-
-    top.controls.show(new app.SearchControlsView({
-      "collection": this.spansCollection
-    }));
-    top.main.show(new Backgrid.Grid({
-      "collection": this.spansCollection,
-      "columns": [{
-        "label": "Begin",
-        "cell": Backgrid.Cell.extend({
-          className: "begin-cell",
-          formatter: {
-            fromRaw: function(rawData, model) {
-              var beginMs = model.get("beginTime")
-              return moment(beginMs).format('YYYY/MM/DD HH:mm:ss,SSS');
-            },
-            toRaw: function(formattedData, model) {
-              return formattedData // data entry not supported for this cell
+    app.root.app.show(new app.SearchView());
+    app.root.app.currentView.controls.show(
+      new app.SearchControlsView({
+        "collection": this.spansCollection
+      }));
+    app.root.app.currentView.main.show(
+      new Backgrid.Grid({
+        "collection": this.spansCollection,
+        "columns": [{
+          "label": "Begin",
+          "cell": Backgrid.Cell.extend({
+            className: "begin-cell",
+            formatter: {
+              fromRaw: function(rawData, model) {
+                var beginMs = model.get("beginTime")
+                return moment(beginMs).format('YYYY/MM/DD HH:mm:ss,SSS');
+              },
+              toRaw: function(formattedData, model) {
+                return formattedData // data entry not supported for this cell
+              }
             }
-          }
-        }),
-        "editable": false,
-        "sortable": false
-      }, {
-        "name": "spanId",
-        "label": "ID",
-        "cell": "string",
-        "editable": false,
-        "sortable": false
-      }, {
-        "name": "processId",
-        "label": "processId",
-        "cell": "string",
-        "editable": false,
-        "sortable": false
-      }, {
-        "label": "Duration",
-        "cell": Backgrid.Cell.extend({
-          className: "duration-cell",
-          formatter: {
-            fromRaw: function(rawData, model) {
-              return model.duration() + " ms"
-            },
-            toRaw: function(formattedData, model) {
-              return formattedData // data entry not supported for this cell
+          }),
+          "editable": false,
+          "sortable": false
+        }, {
+          "name": "spanId",
+          "label": "ID",
+          "cell": "string",
+          "editable": false,
+          "sortable": false
+        }, {
+          "name": "processId",
+          "label": "processId",
+          "cell": "string",
+          "editable": false,
+          "sortable": false
+        }, {
+          "label": "Duration",
+          "cell": Backgrid.Cell.extend({
+            className: "duration-cell",
+            formatter: {
+              fromRaw: function(rawData, model) {
+                return model.duration() + " ms"
+              },
+              toRaw: function(formattedData, model) {
+                return formattedData // data entry not supported for this cell
+              }
             }
+          }),
+          "editable": false,
+          "sortable": false
+        }, {
+          "name": "description",
+          "label": "Description",
+          "cell": "string",
+          "editable": false,
+          "sortable": false
+        }],
+        "row": Backgrid.Row.extend({
+          "events": {
+            "click": "details"
+          },
+          "details": function() {
+            Backbone.history.navigate("!/spans/" + this.model.get("spanId"), {"trigger": true});
           }
-        }),
-        "editable": false,
-        "sortable": false
-      }, {
-        "name": "description",
-        "label": "Description",
-        "cell": "string",
-        "editable": false,
-        "sortable": false
-      }],
-      "row": Backgrid.Row.extend({
-        "events": {
-          "click": "details"
-        },
-        "details": function() {
-          Backbone.history.navigate("!/spans/" + this.model.get("spanId"), {"trigger": true});
-        }
-      })
-    }));
-    top.pagination.show(new Backgrid.Extension.Paginator({
-      collection: this.spansCollection,
-    }));
+        })
+      }));
+    app.root.app.currentView.pagination.show(
+      new Backgrid.Extension.Paginator({
+        collection: this.spansCollection,
+      }));
   },
 
   "span": function(id) {
@@ -127,17 +128,21 @@
       return;
     }
 
-    var top = new app.DetailsView();
-    app.root.app.show(top);
-    top.span.show(new app.SpanDetailsView({
-      "model": span
-    }));
-    top.content.show(new app.GraphView({
+    var graphView = new app.GraphView({
       "collection": this.spansCollection,
-      "spanId": id,
-      "el": top.content.$el[0],
-      "id": "span-" + id
-    }));
+      "id": "span-graph"
+    });
+
+    graphView.on("update:span", function(d) {
+      app.root.app.currentView.span.show(
+        new app.SpanDetailsView({
+          "model": d.span
+        }));
+    });
+
+    app.root.app.show(new app.DetailsView());
+    app.root.app.currentView.content.show(graphView);
+    app.root.app.currentView.content.currentView.setSpanId(id);
   },
 
   "swimlane": function(id, lim) {
diff --git a/htrace-core/src/web/app/views/graph/graph.js b/htrace-core/src/web/app/views/graph/graph.js
index def16d3..7b4f89e 100644
--- a/htrace-core/src/web/app/views/graph/graph.js
+++ b/htrace-core/src/web/app/views/graph/graph.js
@@ -18,42 +18,43 @@
  */
 
 app.GraphView = Backbone.View.extend({
-  "MAX_NODE_SIZE": 100,
-  "MIN_NODE_SIZE": 30,
-
   initialize: function(options) {
     options = options || {};
-    _.bindAll(this, "render");
-    this.collection.bind('change', this.render);
 
     if (!options.id) {
       console.error("GraphView requires argument 'id' to uniquely identify this graph.");
       return;
     }
-    if (!options.el) {
-      console.error("GraphView requires argument 'el' to bind the graph to.");
-      return;
-    }
-    if (!options.spanId) {
-      console.error("GraphView requires argument 'spanId' as a start point.");
-      return;
-    }
 
-    this.spanId = options.spanId;
+    _.bindAll(this, "render");
+    this.collection.bind('change', this.render);
 
-    window.force = this.force
-        = d3.layout.force().size([1000, 1000])
-                           .alpha(0.1)
-                           .linkDistance(this.MAX_NODE_SIZE*2)
-                           .charge(-600)
-                           .linkDistance(150)
-                           .linkStrength(1)
-                           .friction(0.2)
-                           // .gravity(0.1)
+    var links = this.links = [];
+    var linkTable = this.linkTable = {};
+    var nodes = this.nodes = [];
+    var nodeTable = this.nodeTable = {};
+    var force = this.force
+        = d3.layout.force().size([$(window).width(), $(window).height() * 3/4])
+                           .linkDistance($(window).height() / 5)
+                           .charge(-120)
+                           .gravity(0)
                            ;
+    force.nodes(nodes)
+         .links(links);
 
     force.on("tick", function(e) {
       var root = d3.select("#" + options.id);
+      
+      if (!root.node()) {
+        return;
+      }
+
+      var selectedDatum = root.select(".selected").datum();
+
+      // center selected node
+      root.select("svg").attr("width", $(root.node()).width());
+      selectedDatum.x = root.select("svg").attr("width") / 2;
+      selectedDatum.y = root.select("svg").attr("height") / 2;
 
       // Push sources up and targets down to form a weak tree.
       var k = 10 * e.alpha;
@@ -62,142 +63,200 @@
         d.target.y += k;
       });
 
-      // set selected node in the middle.
-      // var selectedNode = force.nodes()[0];
-      // selectedNode.x = 500;
-      // selectedNode.y = 500;
-
       var nodes = root.selectAll(".node").data(force.nodes());
       nodes.select("circle")
         .attr("cx", function(d) { return d.x; })
         .attr("cy", function(d) { return d.y; });
       nodes.select("text")
-        .attr("x", function(d) { return d.x; })
+        .attr("x", function(d) { return d.x - this.getComputedTextLength() / 2; })
         .attr("y", function(d) { return d.y; });
       root.selectAll(".link").data(force.links())
-        .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; });
+        .attr("d", function(d) {
+          var start = {},
+              end = {},
+              angle = Math.atan2((d.target.x - d.source.x), (d.target.y - d.source.y));
+          start.x = d.source.x + d.source.r * Math.sin(angle);
+          end.x = d.target.x - d.source.r * Math.sin(angle);
+          start.y = d.source.y + d.source.r * Math.cos(angle);
+          end.y = d.target.y - d.source.r * Math.cos(angle);
+          return "M" + start.x + " " + start.y
+              + " L" + end.x + " " + end.y;
+        });
     });
   },
 
-  parents: function(span) {
-    var collection = this.collection;
-    return _(span.get("parents")).map(function(parentSpanId) {
-      collection.findWhere({
-        "spanId": parentSpanId
-      });
-    });
-  },
+  updateLinksAndNodes: function() {
+    if (!this.spanId) {
+      return;
+    }
 
-  children: function(span) {
-    var spanId = span.get("spanId");
-    return this.collection.filter(function(model) {
-      return _(model.get("parents")).contains(spanId);
-    });
-  },
+    var $this = this, collection = this.collection;
 
-  linksAndNodes: function() {
-    var links = [],
-        nodes = [];
     var selectedSpan = this.collection.findWhere({
       "spanId": this.spanId
     });
-    var parents = this.parents(selectedSpan);
-    var children = this.children(selectedSpan);
 
-    var group = 0;
-    var spanToNode = (function() {
-      var xmap = {};
-      return function(span, level) {
-        return {
-          "name": span.get("description"),
-          "span": span
+    var findChildren = function(span) {
+      var spanId = span.get("spanId");
+      var spans = collection.filter(function(model) {
+        return _(model.get("parents")).contains(spanId);
+      });
+      return _(spans).reject(function(span) {
+        return span == null;
+      });
+    };
+    var findParents = function(span) {
+      var spans = _(span.get("parents")).map(function(parentSpanId) {
+        return collection.findWhere({
+          "spanId": parentSpanId
+        });
+      });
+      return _(spans).reject(function(span) {
+        return span == null;
+      });
+    };
+    var spanToNode = function(span, level) {
+      var table = $this.nodeTable;
+      if (!(span.get("spanId") in table)) {
+        table[span.get("spanId")] = {
+          "name": span.get("spanId"),
+          "span": span,
+          "level": level,
+          "group": 0,
+          "x": parseInt($this.svg.attr('width')) / 2,
+          "y": 250 + level * 50
         };
+        $this.nodes.push(table[span.get("spanId")]);
       }
-    })();
+
+      return table[span.get("spanId")];
+    };
     var createLink = function(source, target) {
-      return {
-        "source": source,
-        "target": target
-      };
+      var table = $this.linkTable;
+      var name = source.span.get("spanId") + "-" + target.span.get("spanId");
+      if (!(name in table)) {
+        table[name] = {
+          "source": source,
+          "target": target
+        };
+        $this.links.push(table[name]);
+      }
+
+      return table[name];
     };
 
+    var parents = [], children = [];
     var selectedSpanNode = spanToNode(selectedSpan, 1);
-    nodes.push(selectedSpanNode);
 
+    Array.prototype.push.apply(parents, findParents(selectedSpan));
     _(parents).each(function(span) {
-      var node = spanToNode(span, 0);
-      nodes.push(node);
-      links.push(createLink(node, selectedSpanNode));
+      Array.prototype.push.apply(parents, findParents(span));
+      createLink(spanToNode(span, 0), selectedSpanNode)
     });
 
+    Array.prototype.push.apply(children, findChildren(selectedSpan));
     _(children).each(function(span) {
-      var node = spanToNode(span, 2);
-      nodes.push(node);
-      links.push(createLink(selectedSpanNode, node));
+      Array.prototype.push.apply(children, findChildren(span));
+      createLink(selectedSpanNode, spanToNode(span, 2))
     });
-
-    return {
-      "links": links,
-      "nodes": nodes
-    };
   },
 
   renderLinks: function(selection) {
-    selection.append("line")
-        .attr("class", "link");
+    var path = selection.enter().append("path")
+        .classed("link", true)
+        .style("marker-end",  "url(#suit)");
+    selection.exit().remove();
     return selection;
   },
 
   renderNodes: function(selection) {
-    var MAX_NODE_SIZE = this.MAX_NODE_SIZE,
-        MIN_NODE_SIZE = this.MIN_NODE_SIZE;
-    var g = selection.append("g").attr("class", "node");
-    g.append("circle")
+    var $this = this;
+    var g = selection.enter().append("g").attr("class", "node");
+    var circle = g.append("circle")
       .attr("r", function(d) {
-        var reduced = Math.log(d.span.duration());
+        if (!d.radius) {
+          d.r = Math.log(d.span.duration());
          
-        if (reduced > MAX_NODE_SIZE) {
-          return MAX_NODE_SIZE;
+          if (d.r > app.GraphView.MAX_NODE_SIZE) {
+            d.r = app.GraphView.MAX_NODE_SIZE;
+          }
+
+          if (d.r < app.GraphView.MIN_NODE_SIZE) {
+            d.r = app.GraphView.MIN_NODE_SIZE;
+          }
         }
 
-        if (reduced < MIN_NODE_SIZE) {
-          return MIN_NODE_SIZE;
-        }
-
-        return reduced;
+        return d.r;
       });
     var text = g.append("text").text(function(d) {
-      return d.name;
+      return d.span.get("description");
     });
+
+    selection.exit().remove();
+
+    circle.on("click", function(d) {
+      $this.setSpanId(d.name);
+    });
+
+    selection.classed("selected", null);
+    selection.filter(function(d) {
+      return d.span.get("spanId") == $this.spanId;
+    }).classed("selected", true);
     
     return selection;
   },
 
+  setSpanId: function(spanId) {
+    var $this = this;
+    this.spanId = spanId;
+
+    this.updateLinksAndNodes();
+
+    this.renderNodes(
+      this.svg.selectAll(".node")
+        .data(this.force.nodes(), function(d) {
+          return d.name;
+        }));
+
+    this.renderLinks(
+      this.svg.selectAll(".link")
+        .data(this.force.links(), function(d) {
+          return d.source.name + "-" + d.target.name;
+        }));
+
+    this.force.start();
+
+    Backbone.history.navigate("!/spans/" + spanId);
+    this.trigger("update:span", {"span": this.collection.findWhere({
+      "spanId": spanId
+    })});
+  },
+
   render: function() {
-    var svg = d3.select(this.$el[0]).append("svg")
-      .attr("height", 1000)
-      .attr("width", 1000)
-      .attr("id", this.id);
-    var data = this.linksAndNodes();
+    this.svg = d3.select(this.$el[0]).append("svg");
+    this.svg.attr("height", 500)
+       .attr("width", $(window).width())
+       .attr("id", this.id);
 
-    this.force
-      .nodes(data.nodes)
-      .links(data.links)
-      .start();
-
-    var link = this.renderLinks(
-      svg.selectAll(".link")
-        .data(this.force.links())
-        .enter());
-
-    var node = this.renderNodes(
-      svg.selectAll(".node")
-        .data(this.force.nodes())
-        .enter());
+    // Arrows
+    this.svg.append("defs").selectAll("marker")
+      .data(["suit", "licensing", "resolved"])
+    .enter().append("marker")
+      .attr("id", function(d) { return d; })
+      .attr("viewBox", "0 -5 10 10")
+      .attr("refX", 25)
+      .attr("refY", 0)
+      .attr("markerWidth", 6)
+      .attr("markerHeight", 6)
+      .attr("orient", "auto")
+    .append("path")
+      .attr("d", "M0,-5L10,0L0,5 L10,0 L0, -5")
+      .style("stroke", "#4679BD")
+      .style("opacity", "0.6");
 
     return this;
   }
 });
+
+app.GraphView.MAX_NODE_SIZE = 150;
+app.GraphView.MIN_NODE_SIZE = 50;
diff --git a/htrace-core/src/web/lib/css/main.css b/htrace-core/src/web/lib/css/main.css
index c0a8f3e..1875b60 100644
--- a/htrace-core/src/web/lib/css/main.css
+++ b/htrace-core/src/web/lib/css/main.css
@@ -29,12 +29,17 @@
 
 /* Graph */
 svg .node circle {
-  stroke: #006600;
+  stroke: #000000;
   stroke-width: 3;
-  fill: #00CC00;
+  fill: #D8BFD8;
+}
+
+svg .selected circle {
+  stroke-width: 3;
+  fill: #DA70D6;
 }
 
 svg .link {
   stroke: black;
   stroke-width: 3;
-}
\ No newline at end of file
+}