diff --git a/static/css/quay.css b/static/css/quay.css
index dc7c2fade..96f3a8a2c 100644
--- a/static/css/quay.css
+++ b/static/css/quay.css
@@ -697,11 +697,11 @@ p.editable:hover i {
position: relative;
}
-#image-history-container .node.highlighted circle {
+#image-history-container .node circle.highlighted {
stroke: steelblue;
}
-#image-history-container .node.current circle {
+#image-history-container .node circle.current {
fill: steelblue;
stroke-width: 2.5px;
}
@@ -844,22 +844,47 @@ p.editable:hover i {
/* Creates a small triangle extender for the tooltip */
.d3-tip:after {
- box-sizing: border-box;
- display: inline;
- font-size: 10px;
- width: 100%;
- line-height: 1;
- color: rgba(0, 0, 0, 0.8);
- content: "\25BC";
- position: absolute;
- text-align: center;
+ box-sizing: border-box;
+ display: inline;
+ font-size: 10px;
+ width: 100%;
+ line-height: 1;
+ color: rgba(0, 0, 0, 0.8);
+ position: absolute;
}
-/* Style northward tooltips differently */
+/* Nrthward tooltips */
.d3-tip.n:after {
- margin: -3px 0 0 0;
- top: 100%;
- left: 0;
+ content: "\25BC";
+ margin: -1px 0 0 0;
+ top: 100%;
+ left: 0;
+ text-align: center;
+}
+
+/* Eastward tooltips */
+.d3-tip.e:after {
+ content: "\25C0";
+ margin: -4px 0 0 0;
+ top: 50%;
+ left: -8px;
+}
+
+/* Southward tooltips */
+.d3-tip.s:after {
+ content: "\25B2";
+ margin: 0 0 1px 0;
+ top: -8px;
+ left: 0;
+ text-align: center;
+}
+
+/* Westward tooltips */
+.d3-tip.w:after {
+ content: "\25B6";
+ margin: -4px 0 0 -1px;
+ top: 50%;
+ left: 100%;
}
.d3-tip .full-id {
diff --git a/static/js/graphing.js b/static/js/graphing.js
index 977c087df..d79f125d1 100644
--- a/static/js/graphing.js
+++ b/static/js/graphing.js
@@ -22,7 +22,7 @@ function ImageHistoryTree(namespace, name, images, current, formatComment, forma
/**
* The current tag.
*/
- this.currentTag_ = current;
+ this.currentTag_ = current.name;
/**
* The current image.
@@ -51,16 +51,18 @@ function ImageHistoryTree(namespace, name, images, current, formatComment, forma
*/
ImageHistoryTree.prototype.draw = function(container) {
// Build the root of the tree.
- var maxHeight = this.buildRoot_();
+ this.maxHeight_ = this.buildRoot_();
+ // Determine the size of the SVG container.
var width = document.getElementById(container).clientWidth;
- var height = Math.max(width * 0.625, maxHeight * (DEPTH_HEIGHT + 10));
+ var height = Math.max(width * 0.625, this.maxHeight_ * (DEPTH_HEIGHT + 10));
- var margin = { top: 40, right: 120, bottom: 20, left: 120 };
+ var margin = { top: 40, right: 60, bottom: 20, left: 60 };
var m = [margin.top, margin.right, margin.bottom, margin.left];
var w = width - m[1] - m[3];
var h = height - m[0] - m[2];
+ // Create the tree and all its components.
var tree = d3.layout.tree()
.size([h, w]);
@@ -77,7 +79,8 @@ ImageHistoryTree.prototype.draw = function(container) {
var formatTime = this.formatTime_;
var tip = d3.tip()
.attr('class', 'd3-tip')
- .offset([-10, 0])
+ .offset([-1, 24])
+ .direction('e')
.html(function(d) {
var html = '';
if (d.image.comment) {
@@ -90,6 +93,7 @@ ImageHistoryTree.prototype.draw = function(container) {
vis.call(tip);
+ // Save all the state created.
this.fullWidth_ = width;
this.width_ = w;
@@ -121,6 +125,18 @@ ImageHistoryTree.prototype.getAncestors = function(image) {
};
+/**
+ * Marks the image node with whether it is the current image and/or highlighted.
+ */
+ImageHistoryTree.prototype.markWithState_ = function(image, imageNode) {
+ var currentAncestors = this.getAncestors(this.currentImage_);
+ var isCurrent = image.id == this.currentImage_.id;
+ var isHighlighted = currentAncestors.indexOf(image.dbid.toString()) >= 0;
+ imageNode.highlighted = isHighlighted || isCurrent;
+ imageNode.current = isCurrent;
+};
+
+
/**
* Builds the root node for the tree.
*/
@@ -131,25 +147,22 @@ ImageHistoryTree.prototype.buildRoot_ = function() {
// "children": [...]
// }
var formatted = {};
- var currentAncestors = this.getAncestors(this.currentImage_);
// Build a node for each image.
var imageByDBID = {};
for (var i = 0; i < this.images_.length; ++i) {
var image = this.images_[i];
- var isCurrent = image.id == this.currentImage_.id;
- var isHighlighted = currentAncestors.indexOf(image.dbid.toString()) >= 0;
var imageNode = {
"name": image.id.substr(0, 12),
"children": [],
"image": image,
- "highlighted": isHighlighted || isCurrent,
- "current": isCurrent,
"tags": image.tags
};
+ this.markWithState_(image, imageNode);
imageByDBID[image.dbid] = imageNode;
}
-
+ this.imageByDBID_ = imageByDBID;
+
// For each node, attach it to its immediate parent. If there is no immediate parent,
// then the node is the root.
var maxAncestorCount = 0;
@@ -160,7 +173,13 @@ ImageHistoryTree.prototype.buildRoot_ = function() {
var immediateParent = ancestors[ancestors.length - 1] * 1;
var parent = imageByDBID[immediateParent];
if (parent) {
- parent.children.push(imageNode);
+ // If the image node is highlighted, ensure it is at the front
+ // of the child list.
+ if (imageNode.highlighted) {
+ parent.children.splice(0, 0, imageNode);
+ } else {
+ parent.children.push(imageNode);
+ }
} else {
formatted = imageNode;
}
@@ -173,6 +192,55 @@ ImageHistoryTree.prototype.buildRoot_ = function() {
};
+/**
+ * Sets the current tag displayed in the tree.
+ */
+ImageHistoryTree.prototype.setTag_ = function(tagName) {
+ this.currentTag_ = tagName;
+
+ // Find the new current image.
+ for (var i = 0; i < this.images_.length; ++i) {
+ var image = this.images_[i];
+ if (image.tags.indexOf(tagName) >= 0) {
+ this.currentImage_ = image;
+ }
+ }
+
+ // Update the state of each node.
+ var imageByDBID = this.imageByDBID_;
+ var currentAncestors = this.getAncestors(this.currentImage_);
+ for (var i = 0; i < this.images_.length; ++i) {
+ var image = this.images_[i];
+ var imageNode = this.imageByDBID_[image.dbid];
+ this.markWithState_(image, imageNode);
+ }
+
+ // Ensure that the children are in the correct order.
+ for (var i = 0; i < this.images_.length; ++i) {
+ var image = this.images_[i];
+ var imageNode = this.imageByDBID_[image.dbid];
+ var ancestors = this.getAncestors(image);
+ var immediateParent = ancestors[ancestors.length - 1] * 1;
+ var parent = imageByDBID[immediateParent];
+ if (parent && imageNode.highlighted) {
+ var arr = parent.children;
+ if (parent._children) {
+ arr = parent._children;
+ }
+
+ if (arr[0] != imageNode) {
+ var index = arr.indexOf(imageNode);
+ arr.splice(index, 1);
+ arr.splice(0, 0, imageNode);
+ }
+ }
+ }
+
+ // Finally, update the tree.
+ this.update_(this.root_);
+};
+
+
/**
* Populates the tree.
*/
@@ -197,6 +265,7 @@ ImageHistoryTree.prototype.update_ = function(source) {
var currentTag = this.currentTag_;
var repoNamespace = this.repoNamespace_;
var repoName = this.repoName_;
+ var maxHeight = this.maxHeight_;
var that = this;
@@ -206,7 +275,7 @@ ImageHistoryTree.prototype.update_ = function(source) {
var nodes = tree.nodes(this.root_).reverse();
// Normalize for fixed-depth.
- nodes.forEach(function(d) { d.y = d.depth * DEPTH_HEIGHT; });
+ nodes.forEach(function(d) { d.y = (maxHeight - d.depth - 1) * DEPTH_HEIGHT; });
// Update the nodes...
var node = vis.selectAll("g.node")
@@ -214,9 +283,7 @@ ImageHistoryTree.prototype.update_ = function(source) {
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("svg:g")
- .attr("class", function(d) {
- return "node " + (d.current ? "current " : "") + (d.highlighted ? "highlighted " : "");
- })
+ .attr("class", "node")
.attr("transform", function(d) { return "translate(" + source.x0 + "," + source.y0 + ")"; });
nodeEnter.append("svg:circle")
@@ -243,29 +310,16 @@ ImageHistoryTree.prototype.update_ = function(source) {
// Create the foreign object to hold the tags (if any).
var fo = g.append("svg:foreignObject")
+ .attr("class", "fo")
.attr("x", 14)
- .attr("y", 10)
+ .attr("y", 12)
.attr("width", 110)
.attr("height", DEPTH_HEIGHT - 20);
- // Add the tags.
+ // Add the tags container.
fo.append('xhtml:div')
.attr("class", "tags")
- .style("display", "none")
- .html(function(d) {
- var html = '';
- for (var i = 0; i < d.tags.length; ++i) {
- var tag = d.tags[i];
- var kind = 'default';
- if (tag == currentTag.name) {
- kind = 'success';
- }
- html += '';
- html += '' + tag + '';
- html += '';
- }
- return html;
- });
+ .style("display", "none");
// Translate the foreign object so the tags are under the ID.
fo.attr("transform", function(d, i) {
@@ -278,9 +332,12 @@ ImageHistoryTree.prototype.update_ = function(source) {
.duration(duration)
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
+ // Update the node circle.
nodeUpdate.select("circle")
.attr("r", 4.5)
- .attr("class", function(d) { return d._children ? "closed" : "open"; })
+ .attr("class", function(d) {
+ return (d._children ? "closed " : "open ") + (d.current ? "current " : "") + (d.highlighted ? "highlighted " : "");
+ })
.style("fill", function(d) {
if (d.current) {
return "";
@@ -288,12 +345,45 @@ ImageHistoryTree.prototype.update_ = function(source) {
return d._children ? "lightsteelblue" : "#fff";
});
- nodeUpdate.select(".tags")
- .style("display", "");
-
+ // Ensure that the node is visible.
nodeUpdate.select("g")
.style("fill-opacity", 1);
+ // Update the tags.
+ node.select(".tags")
+ .html(function(d) {
+ var html = '';
+ for (var i = 0; i < d.tags.length; ++i) {
+ var tag = d.tags[i];
+ var kind = 'default';
+ if (tag == currentTag) {
+ kind = 'success';
+ }
+ html += '' + tag + '';
+ }
+ return html;
+ });
+
+ // Listen for click events on the labels.
+ node.selectAll(".tag")
+ .on("click", function(d, e) {
+ var tag = this.getAttribute('data-tag');
+ if (tag) {
+ that.setTag_(tag);
+ }
+ });
+
+ // Ensure the tags are visible.
+ nodeUpdate.select(".tags")
+ .style("display", "")
+
+ // There is a bug in Chrome which sometimes prevents the foreignObject from redrawing. To that end,
+ // we force a redraw by adjusting the height of the object ever so slightly.
+ nodeUpdate.select(".fo")
+ .attr('height', function(d) {
+ return DEPTH_HEIGHT - 20 + Math.random() / 10;
+ });
+
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
@@ -330,7 +420,11 @@ ImageHistoryTree.prototype.update_ = function(source) {
// Transition links to their new position.
link.transition()
.duration(duration)
- .attr("d", diagonal);
+ .attr("d", diagonal)
+ .attr("class", function(d) {
+ var isHighlighted = d.target.highlighted;
+ return "link " + (isHighlighted ? "highlighted": "");
+ });
// Transition exiting nodes to the parent's new position.
link.exit().transition()