Reverse the direction of the tree and make it dynamically change the current tag.
This commit is contained in:
parent
4c189f38c2
commit
a7f5b5e033
2 changed files with 172 additions and 53 deletions
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
Reference in a new issue