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
+}