var DEPTH_HEIGHT = 100;
/**
* Based off of http://mbostock.github.io/d3/talk/20111018/tree.html by Mike Bostock (@mbostock)
*/
function ImageHistoryTree(namespace, name, images, current, formatComment, formatTime) {
/**
* The namespace of the repo.
*/
this.repoNamespace_ = namespace;
/**
* The name of the repo.
*/
this.repoName_ = name;
/**
* The images to display.
*/
this.images_ = images;
/**
* The current tag.
*/
this.currentTag_ = current.name;
/**
* The current image.
*/
this.currentImage_ = current.image;
/**
* Counter for creating unique IDs.
*/
this.idCounter_ = 0;
/**
* Method to invoke to format a comment for an image.
*/
this.formatComment_ = formatComment;
/**
* Method to invoke to format the time for an image.
*/
this.formatTime_ = formatTime;
}
/**
* Draws the tree.
*/
ImageHistoryTree.prototype.draw = function(container) {
// Build the root of the tree.
this.maxHeight_ = this.buildRoot_();
// Determine the size of the SVG container.
var width = document.getElementById(container).clientWidth;
var height = Math.max(width * 0.625, this.maxHeight_ * (DEPTH_HEIGHT + 10));
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]);
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.x, d.y]; });
var vis = d3.select("#" + container).append("svg:svg")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
var formatComment = this.formatComment_;
var formatTime = this.formatTime_;
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-1, 24])
.direction('e')
.html(function(d) {
var html = '';
if (d.image.comment) {
html += '';
}
html += '' + formatTime(d.image.created) + '';
html += '' + d.image.id + '';
return html;
})
vis.call(tip);
// Save all the state created.
this.fullWidth_ = width;
this.width_ = w;
this.height_ = h;
this.diagonal_ = diagonal;
this.vis_ = vis;
this.tip_ = tip;
this.tree_ = tree;
// Populate the tree.
this.populate_();
};
/**
* Returns the ancestors of the given image.
*/
ImageHistoryTree.prototype.getAncestors = function(image) {
var ancestorsString = image.ancestors;
// Remove the starting and ending /s.
ancestorsString = ancestorsString.substr(1, ancestorsString.length - 2);
// Split based on /.
ancestors = ancestorsString.split('/');
return ancestors;
};
/**
* 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.
*/
ImageHistoryTree.prototype.buildRoot_ = function() {
// Build the formatted JSON block for the tree. It must be of the form:
// {
// "name": "...",
// "children": [...]
// }
var formatted = {};
// Build a node for each image.
var imageByDBID = {};
for (var i = 0; i < this.images_.length; ++i) {
var image = this.images_[i];
var imageNode = {
"name": image.id.substr(0, 12),
"children": [],
"image": image,
"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;
for (var i = 0; i < this.images_.length; ++i) {
var image = this.images_[i];
var imageNode = imageByDBID[image.dbid];
var ancestors = this.getAncestors(image);
var immediateParent = ancestors[ancestors.length - 1] * 1;
var parent = imageByDBID[immediateParent];
if (parent) {
// 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;
}
maxAncestorCount = Math.max(maxAncestorCount, ancestors.length);
}
this.root_ = formatted;
return maxAncestorCount + 1;
};
/**
* 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.
*/
ImageHistoryTree.prototype.populate_ = function() {
// Set the position of the initial node.
this.root_.x0 = this.fullWidth_ / 2;
this.root_.y0 = 0;
// Initialize the display to show the current path of nodes.
this.update_(this.root_);
};
/**
* Updates the tree in response to a click on a node.
*/
ImageHistoryTree.prototype.update_ = function(source) {
var tree = this.tree_;
var vis = this.vis_;
var diagonal = this.diagonal_;
var tip = this.tip_;
var currentTag = this.currentTag_;
var repoNamespace = this.repoNamespace_;
var repoName = this.repoName_;
var maxHeight = this.maxHeight_;
var that = this;
var duration = 500;
// Compute the new tree layout.
var nodes = tree.nodes(this.root_).reverse();
// Normalize for fixed-depth.
nodes.forEach(function(d) { d.y = (maxHeight - d.depth - 1) * DEPTH_HEIGHT; });
// Update the nodes...
var node = vis.selectAll("g.node")
.data(nodes, function(d) { return d.id || (d.id = that.idCounter_++); });
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("svg:g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + source.x0 + "," + source.y0 + ")"; });
nodeEnter.append("svg:circle")
.attr("r", 1e-6)
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; })
.on("click", function(d) { that.toggle_(d); that.update_(d); });
// Create the group that will contain the node name and its tags.
var g = nodeEnter.append("svg:g").style("fill-opacity", 1e-6);
// Add the repo ID.
g.append("svg:text")
.attr("x", function(d) { return d.children || d._children ? -10 : 10; })
.attr("dy", ".35em")
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
.text(function(d) { return d.name; })
.on("click", function(d) { that.toggle_(d); that.update_(d); })
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
nodeEnter.selectAll("tags")
.append("svg:text")
.text("bar");
// Create the foreign object to hold the tags (if any).
var fo = g.append("svg:foreignObject")
.attr("class", "fo")
.attr("x", 14)
.attr("y", 12)
.attr("width", 110)
.attr("height", DEPTH_HEIGHT - 20);
// Add the tags container.
fo.append('xhtml:div')
.attr("class", "tags")
.style("display", "none");
// Translate the foreign object so the tags are under the ID.
fo.attr("transform", function(d, i) {
bbox = this.getBBox()
return "translate(" + [-130, 0 ] + ")";
});
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.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 ") + (d.current ? "current " : "") + (d.highlighted ? "highlighted " : "");
})
.style("fill", function(d) {
if (d.current) {
return "";
}
return d._children ? "lightsteelblue" : "#fff";
});
// 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)
.attr("transform", function(d) { return "translate(" + source.x + "," + source.y + ")"; })
.remove();
nodeExit.select("circle")
.attr("r", 1e-6);
nodeExit.select(".tags")
.style("display", "none");
nodeExit.select("g")
.style("fill-opacity", 1e-6);
// Update the links...
var link = vis.selectAll("path.link")
.data(tree.links(nodes), function(d) { return d.target.id; });
// Enter any new links at the parent's previous position.
link.enter().insert("svg:path", "g")
.attr("class", function(d) {
var isHighlighted = d.target.highlighted;
return "link " + (isHighlighted ? "highlighted": "");
})
.attr("d", function(d) {
var o = {x: source.x0, y: source.y0};
return diagonal({source: o, target: o});
})
.transition()
.duration(duration)
.attr("d", diagonal);
// Transition links to their new position.
link.transition()
.duration(duration)
.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()
.duration(duration)
.attr("d", function(d) {
var o = {x: source.x, y: source.y};
return diagonal({source: o, target: o});
})
.remove();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
};
/**
* Toggles children of a node.
*/
ImageHistoryTree.prototype.toggle_ = function(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
};