From a7f5b5e0339a9c0de2a52baef48e573b4145dc07 Mon Sep 17 00:00:00 2001 From: Joseph Schorr <jschorr@gmail.com> Date: Thu, 10 Oct 2013 17:13:42 -0400 Subject: [PATCH] Reverse the direction of the tree and make it dynamically change the current tag. --- static/css/quay.css | 55 ++++++++++---- static/js/graphing.js | 170 ++++++++++++++++++++++++++++++++---------- 2 files changed, 172 insertions(+), 53 deletions(-) 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 += '<a href="/#/repository/' + repoNamespace + '/' + repoName + '/tag/' + tag + '">'; - html += '<span class="label label-' + kind + ' tag">' + tag + '</span>'; - html += '</a>'; - } - 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 += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '">' + tag + '</span>'; + } + 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()