HTRACE-131. Port spans.js in htrace-hbase to htraced (Masatake Iwasaki via Colin P. McCabe)
diff --git a/htrace-core/src/web/app/setup.js b/htrace-core/src/web/app/setup.js
index d210ea6..90915e3 100644
--- a/htrace-core/src/web/app/setup.js
+++ b/htrace-core/src/web/app/setup.js
@@ -29,7 +29,9 @@
   "routes": {
     "": "init",
     "!/search(/:query)": "search",
-    "!/spans/:id": "span"
+    "!/spans/:id": "span",
+    "!/swimlane/:id": "swimlane",
+    "!/swimlane/:id:?:lim": "swimlane"
   },
 
   "initialize": function() {
@@ -131,6 +133,15 @@
       "el": top.content.$el[0],
       "id": "span-" + id
     }));
+  },
+
+  "swimlane": function(id, lim) {
+    var top = new app.SwimlaneView();
+    app.root.app.show(top);
+    top.swimlane.show(new app.SwimlaneGraphView({
+      "spanId": id,
+      "lim": lim
+    }));
   }
 });
 
diff --git a/htrace-core/src/web/app/views/details/details.js b/htrace-core/src/web/app/views/details/details.js
index 2a2b9f4..2f79e1b 100644
--- a/htrace-core/src/web/app/views/details/details.js
+++ b/htrace-core/src/web/app/views/details/details.js
@@ -35,5 +35,13 @@
     };
     context["span"]["duration"] = this.model.duration();
     return context;
+  },
+  
+  "events": {
+    "click": "swimlane"
+  },
+  "swimlane": function() {
+    Backbone.history.navigate("!/swimlane/" + this.model.get("spanId"),
+                              {"trigger": true});
   }
 });
diff --git a/htrace-core/src/web/app/views/swimlane/swimlane.js b/htrace-core/src/web/app/views/swimlane/swimlane.js
new file mode 100644
index 0000000..878e2d0
--- /dev/null
+++ b/htrace-core/src/web/app/views/swimlane/swimlane.js
@@ -0,0 +1,178 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+app.SwimlaneView = Backbone.Marionette.LayoutView.extend({
+  "template": "#swimlane-layout-template",
+  "regions": {
+    "swimlane": "div[role='complementary']",
+    "content": "div[role='main']"
+  }
+});
+
+app.SwimlaneGraphView = Backbone.Marionette.View.extend({
+  className: "swimlane",
+
+  initialize: function() {
+    this.spans = this.getSpans(0, [], 
+                               this.getJsonSync("/span/" + this.options.spanId),
+                               this.options.lim || "lim=100",
+                               this.getJsonSync);
+  },
+  
+  render: function() {
+    this.appendSVG(this.spans);
+  },
+
+  getSpans: function getSpans(depth, spans, span, lim, getJSON) {
+    span.depth = depth;
+    spans.push(span);
+    var children = [];
+    getJSON("/span/" + span.s + "/children?" + lim).forEach(function(childId) {
+      children.push(getJSON("/span/" + childId));
+    });
+    children.sort(function(x, y) {
+      return x.b < y.b ? -1 : x.b > y.b ? 1 : 0;
+    });
+    children.forEach(function(child) {
+      spans = getSpans(depth + 1, spans, child, lim, getJSON);
+    });
+    return spans;
+  },
+
+  getJsonSync: function getJsonSync(url) {
+    return $.ajax({
+      type: "GET",
+      url: url,
+      async: false,
+      dataType: "json"
+    }).responseJSON;
+  },
+
+  appendSVG: function appendSVG(spans) {
+    const height_span = 20;
+    const width_span = 700;
+    const size_tl = 6;
+    const margin = {top: 50, bottom: 50, left: 20, right: 1000, process: 300};
+
+    var height_screen = spans.length * height_span;
+    var dmax = d3.max(spans, function(s) { return s.depth; });
+    var tmin = d3.min(spans, function(s) { return s.b; });
+    var tmax = d3.max(spans, function(s) { return s.e; });
+    var xscale = d3.time.scale()
+      .domain([new Date(tmin), new Date(tmax)]).range([0, width_span]);
+
+    var svg = d3.select("div[role='main']").append("svg")
+      .attr("width", width_span + margin.process + margin.left + margin.right)
+      .attr("height", height_screen + margin.top + margin.bottom)
+      .append("g")
+      .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
+
+    var bars = svg.append("g")
+      .attr("id", "bars")
+      .attr("width", width_span)
+      .attr("height", height_screen)
+      .attr("transform", "translate(" + (10 * dmax + margin.process) + ", 0)");
+    
+    var span_g = bars.selectAll("g.span")
+      .data(spans)
+      .enter()
+      .append("g")
+      .attr("transform", function(s, i) {
+        return "translate(0, " + (i * height_span + 5) + ")";
+      })
+      .classed("timeline", function(d) { return d.t; });
+
+    span_g.append("text")
+      .text(function(s) { return s.r; })
+      .style("alignment-baseline", "hanging")
+      .style("font-size", "14px")
+      .attr("transform", function(s) {
+        return "translate(" + (s.depth * 10 - margin.process - 10 * dmax) + ", 0)";
+      });
+
+    var rect_g = span_g.append("g")
+      .attr("transform", function(s) {
+        return "translate(" + xscale(new Date(s.b)) + ", 0)";
+      });
+
+    rect_g.append("rect")
+      .attr("height", height_span - 1)
+      .attr("width", function (s) {
+        return (width_span * (s.e - s.b)) / (tmax - tmin) + 1;
+      })
+      .style("fill", "lightblue")
+      .attr("class", "span")
+
+    rect_g.append("text")
+      .text(function(s){ return s.d; })
+      .style("alignment-baseline", "hanging")
+      .style("font-size", "14px");
+
+    rect_g.append("text")
+      .text(function(s){ return s.e - s.b; })
+      .style("alignment-baseline", "baseline")
+      .style("text-anchor", "end")
+      .style("font-size", "10px")
+      .attr("transform", function(s, i) { return "translate(0, 10)"; });
+
+    bars.selectAll("g.timeline").selectAll("rect.timeline")
+      .data(function(s) { return s.t; })
+      .enter()
+      .append("rect")
+      .style("fill", "red")
+      .attr("height", size_tl)
+      .attr("width", size_tl)
+      .attr("transform", function(t) {
+        return "translate(" + xscale(t.t) + "," + (height_span - 1 - size_tl) + ")";
+      })
+      .classed("timeline");
+
+    var popup = d3.select("div[role='main']").append("div")
+      .attr("class", "popup")
+      .style("opacity", 0);
+
+    bars.selectAll("g.timeline")
+      .on("mouseover", function(d) {
+        popup.transition().duration(300).style("opacity", .95);
+        var text = "<table>";
+        d.t.forEach(function (t) {
+          text += "<tr><td>" + (t.t - tmin) + "</td>";
+          text += "<td> : " + t.m + "<td/></tr>";
+        });
+        text += "</table>"
+        popup.html(text)
+          .style("left", (document.body.scrollLeft + 50) + "px")
+          .style("top", (document.body.scrollTop + 70) + "px")
+          .style("width", "700px")
+          .style("background", "orange")
+          .style("position", "absolute");
+      })
+      .on("mouseout", function(d) {
+        popup.transition().duration(300).style("opacity", 0);
+      });
+    
+    var axis = d3.svg.axis()
+      .scale(xscale)
+      .orient("top")
+      .tickValues(xscale.domain())
+      .tickFormat(d3.time.format("%x %X.%L"))
+      .tickSize(6, 3);
+
+    bars.append("g").attr("class", "axis").call(axis);
+  }
+});
diff --git a/htrace-core/src/web/index.html b/htrace-core/src/web/index.html
index ecb0d7e..bcfea19 100644
--- a/htrace-core/src/web/index.html
+++ b/htrace-core/src/web/index.html
@@ -164,6 +164,18 @@
     </table>
     </script>
 
+    <script id="swimlane-layout-template" type="text/html">
+    <div class="container-fluid" id="list" role="application">
+      <div class="row">
+        <div class="col-md-12" role="main"></div>
+      </div>
+
+      <div class="row">
+        <div class="col-md-12" role="complementary"></div>
+      </div>
+    </div>
+    </script>
+
     <script src="lib/js/jquery-2.1.3.min.js" type="text/javascript"></script>
     <script src="lib/js/d3-3.5.5.js" type="text/javascript"></script>
     <script src="lib/bootstrap-3.3.1/js/bootstrap.min.js" type="text/javascript"></script>
@@ -182,6 +194,7 @@
     <script src="app/views/search/field.js" type="text/javascript"></script>
     <script src="app/views/search/search.js" type="text/javascript"></script>
     <script src="app/views/details/details.js" type="text/javascript"></script>
+    <script src="app/views/swimlane/swimlane.js" type="text/javascript"></script>
     <script src="app/setup.js" type="text/javascript"></script>
   </body>
 </html>