/** * Based off of http://mbostock.github.io/d3/talk/20111018/tree.html by Mike Bostock (@mbostock) and modifications * from http://stackoverflow.com/questions/18108960/d3-tree-layout-custom-vertical-layout-when-children-exceed-more-than-a-certain */ function ImageHistoryTree(images, current, formatComment, formatTime) { /** * The images to display. */ this.images_ = images; /** * The current image. */ this.currentImage_ = current; /** * 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) { var width = document.getElementById(container).clientWidth; var height = width * 0.625; var margin = { top: 40, right: 120, bottom: 20, left: 120 }; var m = [margin.top, margin.right, margin.bottom, margin.left]; var w = width - m[1] - m[3]; var h = height - m[0] - m[2]; 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([-10, 0]) .html(function(d) { var html = ''; if (d.image.comment) { html += '' + formatComment(d.image.comment) + ''; } html += '' + formatTime(d.image.created) + ''; html += '' + d.image.id + ''; return html; }) vis.call(tip); this.width_ = w; this.height_ = h; this.diagonal_ = diagonal; this.vis_ = vis; this.tip_ = tip; this.tree_ = tree; this.populate_(); }; /** * Populates the tree. */ ImageHistoryTree.prototype.populate_ = function() { // Build the formatted JSON block for the tree. It must be of the form: // { // "name": "...", // "children": [...] // } var formatted = {}; var currentAncestors = this.currentImage_.ancestors.split('/'); // 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, "highlighted": jQuery.inArray(currentAncestors, image.dbid.toString()), "current": image.id == this.currentImage_.id }; imageByDBID[image.dbid] = imageNode; } // For each node, attach it to its immediate parent. If there is no immediate parent, // then the node is the root. for (var i = 0; i < this.images_.length; ++i) { var image = this.images_[i]; var imageNode = imageByDBID[image.dbid]; var ancestors = image.ancestors.split('/'); var immediateParent = ancestors[ancestors.length - 2] * 1; var parent = imageByDBID[immediateParent]; if (parent) { parent.children.push(imageNode); } else { formatted = imageNode; } } formatted.children.push({ "name": "bar" }); this.root_ = formatted; // Set the position of the initial node. this.root_.x0 = this.width_ / 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 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 = d.depth * 180; if (d.parent != null) { d.x = d.parent.x - (d.parent.children.length-1)*30/2 + (d.parent.children.indexOf(d))*30; } // if the node has too many children, go in and fix their positions to two columns. if (d.children != null && d.children.length > 4) { d.children.forEach(function (d, i) { d.y = (d.depth * 180 + i % 2 * 100); d.x = d.parent.x - (d.parent.children.length-1)*30/4 + (d.parent.children.indexOf(d))*30/2 - i % 2 * 15; }); } }); // 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", function(d) { return "node " + (d.current ? "current " : "") + (d.highlighted ? "highlighted " : ""); }) .attr("transform", function(d) { return "translate(" + source.x0 + "," + source.y0 + ")"; }) .on("click", function(d) { that.toggle_(d); that.update_(d); }); nodeEnter.append("svg:circle") .attr("r", 1e-6) .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; }); nodeEnter.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; }) .style("fill-opacity", 1e-6) .on('mouseover', tip.show) .on('mouseout', tip.hide); // Transition nodes to their new position. var nodeUpdate = node.transition() .duration(duration) .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }); nodeUpdate.select("circle") .attr("r", 4.5) .attr("class", function(d) { return d._children ? "closed" : "open"; }) .style("fill", function(d) { if (d.current) { return ""; } return d._children ? "lightsteelblue" : "#fff"; }); nodeUpdate.select("text") .style("fill-opacity", 1); // 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("text") .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); // 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; } };