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()