var DEPTH_HEIGHT = 100; var DEPTH_WIDTH = 132; /** * Based off of http://mbostock.github.io/d3/talk/20111018/tree.html by Mike Bostock (@mbostock) */ function ImageHistoryTree(namespace, name, images, 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; /** * 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; /** * The current tag (if any). */ this.currentTag_ = null; /** * The current image (if any). */ this.currentImage_ = null; /** * Counter for creating unique IDs. */ this.idCounter_ = 0; } /** * Calculates the dimensions of the tree. */ ImageHistoryTree.prototype.calculateDimensions_ = function(container) { var cw = Math.max(document.getElementById(container).clientWidth, this.maxWidth_ * DEPTH_WIDTH); var ch = this.maxHeight_ * (DEPTH_HEIGHT + 10); var margin = { top: 40, right: 20, bottom: 20, left: 40 }; var m = [margin.top, margin.right, margin.bottom, margin.left]; var w = cw - m[1] - m[3]; var h = ch - m[0] - m[2]; return { 'w': w, 'h': h, 'm': m, 'cw': cw, 'ch': ch }; }; /** * Updates the dimensions of the tree. */ ImageHistoryTree.prototype.updateDimensions_ = function() { var container = this.container_; var dimensions = this.calculateDimensions_(container); var m = dimensions.m; var w = dimensions.w; var h = dimensions.h; var cw = dimensions.cw; var ch = dimensions.ch; // Set the height of the container so that it never goes offscreen. $('#' + container).removeOverscroll(); var viewportHeight = $(window).height(); var boundingBox = document.getElementById(container).getBoundingClientRect(); document.getElementById(container).style.maxHeight = (viewportHeight - boundingBox.top - 30) + 'px'; $('#' + container).overscroll(); // Update the tree. var rootSvg = this.rootSvg_; var tree = this.tree_; var vis = this.vis_; rootSvg .attr("width", w + m[1] + m[3]) .attr("height", h + m[0] + m[2]); tree.size([w, h]); vis.attr("transform", "translate(" + m[3] + "," + m[0] + ")"); return dimensions; }; /** * Draws the tree. */ ImageHistoryTree.prototype.draw = function(container) { // Build the root of the tree. var result = this.buildRoot_(); this.maxWidth_ = result['maxWidth']; this.maxHeight_ = result['maxHeight']; // Save the container. this.container_ = container; // Create the tree and all its components. var tree = d3.layout.tree() .separation(function() { return 2; }); var diagonal = d3.svg.diagonal() .projection(function(d) { return [d.x, d.y]; }); var rootSvg = d3.select("#" + container).append("svg:svg") .attr("class", "image-tree"); var vis = rootSvg.append("svg:g"); 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.collapsed) { for (var i = 1; i < d.encountered.length; ++i) { html += '' + d.encountered[i].image.id.substr(0, 12) + ''; html += '' + formatTime(d.encountered[i].image.created) + ''; } return html; } if (!d.image) { return '(This repository is empty)'; } if (d.image.comment) { html += '' + formatComment(d.image.comment) + ''; } html += '' + formatTime(d.image.created) + ''; html += '' + d.image.id + ''; return html; }) vis.call(tip); // Save all the state created. this.diagonal_ = diagonal; this.vis_ = vis; this.rootSvg_ = rootSvg; this.tip_ = tip; this.tree_ = tree; // Update the dimensions of the tree. var dimensions = this.updateDimensions_(); // Populate the tree. this.root_.x0 = dimensions.cw / 2; this.root_.y0 = 0; this.setTag_(this.currentTag_); $('#' + container).overscroll(); }; /** * Redraws the image history to fit the new size. */ ImageHistoryTree.prototype.notifyResized = function() { this.updateDimensions_(); this.update_(this.root_); }; /** * Sets the current tag displayed in the tree. */ ImageHistoryTree.prototype.setTag = function(tagName) { this.setTag_(tagName); }; /** * Sets the current image displayed in the tree. */ ImageHistoryTree.prototype.setImage = function(imageId) { this.setImage_(imageId); }; /** * 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; }; /** * Sets the current tag displayed in the tree and raises the event that the tag * was changed. */ ImageHistoryTree.prototype.changeTag_ = function(tagName) { $(this).trigger({ 'type': 'tagChanged', 'tag': tagName }); this.setTag_(tagName); }; /** * Sets the current image displayed in the tree and raises the event that the image * was changed. */ ImageHistoryTree.prototype.changeImage_ = function(imageId) { $(this).trigger({ 'type': 'imageChanged', 'image': this.findImage_(function(image) { return image.id == imageId; }) }); this.setImage_(imageId); }; /** * 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 = {"name": "No images found"}; // 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 }; 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. 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) { // Add a reference to the parent. This makes walking the tree later easier. imageNode.parent = parent; parent.children.push(imageNode); } else { formatted = imageNode; } } // Determine the maximum number of nodes at a particular level. This is used to size // the width of the tree properly. var maxChildCount = 0; for (var i = 0; i < this.images_.length; ++i) { var image = this.images_[i]; var imageNode = imageByDBID[image.dbid]; maxChildCount = Math.max(maxChildCount, this.determineMaximumChildCount_(imageNode)); } // Compact the graph so that any single chain of three (or more) images becomes a collapsed // section. We only do this if the max width is > 1 (since for a single width tree, no long // chain will hide a branch). if (maxChildCount > 1) { this.collapseNodes_(formatted); } // Determine the maximum height of the tree. var maxHeight = this.determineMaximumHeight_(formatted); // Finally, set the root node and return. this.root_ = formatted; return { 'maxWidth': maxChildCount + 1, 'maxHeight': maxHeight }; }; /** * Collapses long single chains of nodes (3 or more) into single nodes to make the graph more * compact. */ ImageHistoryTree.prototype.determineMaximumHeight_ = function(node) { var maxHeight = 0; if (node.children) { for (var i = 0; i < node.children.length; ++i) { maxHeight = Math.max(this.determineMaximumHeight_(node.children[i]), maxHeight); } } return maxHeight + 1; }; /** * Collapses long single chains of nodes (3 or more) into single nodes to make the graph more * compact. */ ImageHistoryTree.prototype.collapseNodes_ = function(node) { if (node.children.length == 1) { // Keep searching downward until we find a node with more than a single child. var current = node; var previous = node; var encountered = []; while (current.children.length == 1) { encountered.push(current); previous = current; current = current.children[0]; } if (encountered.length >= 3) { // Collapse the node. var collapsed = { "name": '(' + (encountered.length - 1) + ' images)', "children": [current], "collapsed": true, "encountered": encountered }; node.children = [collapsed]; // Update the parent relationships. collapsed.parent = node; current.parent = collapsed; return; } } for (var i = 0; i < node.children.length; ++i) { this.collapseNodes_(node.children[i]); } }; /** * Determines the maximum child count for the node and its children. */ ImageHistoryTree.prototype.determineMaximumChildCount_ = function(node) { var children = node.children; var myLevelCount = children.length; var nestedCount = 0; for (var i = 0; i < children.length; ++i) { nestedCount += children[i].children.length; } return Math.max(myLevelCount, nestedCount); }; /** * Finds the image where the checker function returns true and returns it or null * if none. */ ImageHistoryTree.prototype.findImage_ = function(checker) { for (var i = 0; i < this.images_.length; ++i) { var image = this.images_[i]; if (checker(image)) { return image; } } return null; }; /** * Marks the full node path from the given starting node on whether it is highlighted. */ ImageHistoryTree.prototype.markPath_ = function(startingNode, isHighlighted) { var currentNode = startingNode; currentNode.current = isHighlighted; while (currentNode != null) { currentNode.highlighted = isHighlighted; currentNode = currentNode.parent; } }; /** * Sets the current tag displayed in the tree. */ ImageHistoryTree.prototype.setTag_ = function(tagName) { if (tagName == this.currentTag_) { return; } var imageByDBID = this.imageByDBID_; // Save the current tag. var previousTagName = this.currentTag_; this.currentTag_ = tagName; // Update the state of each existing node to no longer be highlighted. var previousImage = this.findImage_(function(image) { return image.tags.indexOf(previousTagName || '(no tag specified)') >= 0; }); if (previousImage) { var currentNode = imageByDBID[previousImage.dbid]; this.markPath_(currentNode, false); } // Find the new current image (if any). this.currentImage_ = this.findImage_(function(image) { return image.tags.indexOf(tagName || '(no tag specified)') >= 0; }); // Update the state of the new node path. if (this.currentImage_) { var currentNode = imageByDBID[this.currentImage_.dbid]; this.markPath_(currentNode, true); } // 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); if (index > 0) { arr.splice(index, 1); arr.splice(0, 0, imageNode); } } } } // Update the tree. this.update_(this.root_); }; /** * Sets the current image highlighted in the tree. */ ImageHistoryTree.prototype.setImage_ = function(imageId) { // Find the new current image. var newImage = this.findImage_(function(image) { return image.id == imageId; }); if (newImage == this.currentImage_) { return; } this.currentImage_ = newImage; 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 currentImage = this.currentImage_; 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) { if (d.image) { that.changeImage_(d.image.id); } }) .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"; }); // Update the repo text. nodeUpdate.select("text") .attr("class", function(d) { if (d.collapsed) { return 'collapsed'; } if (!currentImage) { return ''; } return d.image.id == currentImage.id ? 'current' : ''; }); // Ensure that the node is visible. nodeUpdate.select("g") .style("fill-opacity", 1); // Update the tags. node.select(".tags") .html(function(d) { if (!d.tags) { return ''; } 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.changeTag_(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; } }; /** * Disposes of the tree. */ ImageHistoryTree.prototype.dispose = function() { var container = this.container_ ; $('#' + container).removeOverscroll(); document.getElementById(container).innerHTML = ''; }; //////////////////////////////////////////////////////////////////////////////// /** * Based off of http://bl.ocks.org/mbostock/1093025 by Mike Bostock (@mbostock) */ function ImageFileChangeTree(image, changes) { /** * The parent image. */ this.image_ = image; /** * The changes being drawn. */ this.changes_ = changes; /** * Counter for creating unique IDs. */ this.idCounter_ = 0; /** * Map from file path to associated tree node. */ this.nodeMap_ = {}; } /** * Calculates the dimensions of the tree. */ ImageFileChangeTree.prototype.calculateDimensions_ = function(container) { var cw = document.getElementById(container).clientWidth; var barHeight = 20; var ch = (this.changes_.length * barHeight) + 40; var margin = { top: 40, right: 00, bottom: 20, left: 20 }; var m = [margin.top, margin.right, margin.bottom, margin.left]; var w = cw - m[1] - m[3]; var h = ch - m[0] - m[2]; var barWidth = cw * 0.8 - m[1] - m[3]; return { 'w': w, 'h': h, 'm': m, 'cw': cw, 'ch': ch, 'bw': barWidth, 'bh': barHeight }; }; /** * Updates the dimensions of the tree. */ ImageFileChangeTree.prototype.updateDimensions_ = function() { var container = this.container_; var dimensions = this.calculateDimensions_(container); var w = dimensions.w; var h = dimensions.h; var m = dimensions.m; // Update the tree. var rootSvg = this.rootSvg_; var tree = this.tree_; var vis = this.vis_; rootSvg .attr("width", w + m[1] + m[3]) .attr("height", h + m[0] + m[2]); tree.size([h, 100]); vis.attr("transform", "translate(" + m[3] + "," + m[0] + ")"); this.barWidth_ = dimensions.bw; this.barHeight_ = dimensions.bh; return dimensions; }; /** * Redraws the image change tree to fit the new size. */ ImageFileChangeTree.prototype.notifyResized = function() { this.updateDimensions_(); this.update_(this.root_); }; /** * Disposes of the tree. */ ImageFileChangeTree.prototype.dispose = function() { var container = this.container_ ; document.getElementById(container).innerHTML = ''; }; /** * Draws the tree. */ ImageFileChangeTree.prototype.draw = function(container) { this.container_ = container; var dimensions = this.calculateDimensions_(container); var w = dimensions.w; var h = dimensions.h; var m = dimensions.m; this.barWidth_ = dimensions.bw; this.barHeight_ = dimensions.bh; var tree = d3.layout.tree() .size([h, 100]); var diagonal = d3.svg.diagonal() .projection(function(d) { return [d.y, d.x]; }); var rootSvg = d3.select("#" + container).append("svg:svg") .attr("width", w) .attr("height", h); var vis = rootSvg .append("svg:g") .attr("transform", "translate(" + m[3] + "," + m[0] + ")"); this.rootSvg_ = rootSvg; this.tree_ = tree; this.diagonal_ = diagonal; this.vis_ = vis; this.populateAndDraw_(); }; /** * Populates the tree and then draws it. */ ImageFileChangeTree.prototype.populateAndDraw_ = function() { // For each change, generate all the node(s) needed for the folders, as well // as the final node (if any) for the file. for (var i = 0; i < this.changes_.length; ++i) { var filepath = this.changes_[i].file; var node = this.buildNodes_(filepath); node.kind = this.changes_[i].kind; } // Sort the children of each node so that folders are on top. var sortByName = function (a, b) { if (a.name > b.name) { return 1; } if (a.name < b.name) { return -1; } return 0; }; var sortFunction = function(a, b) { var hasA = a._children.length > 0; var hasB = b._children.length > 0; if (hasA == hasB) { // Sort alphabetically. return sortByName(a, b); } if (hasA) { return -1; } return 1; }; for (var path in this.nodeMap_) { if (!this.nodeMap_.hasOwnProperty(path) || !this.nodeMap_[path]._children) { continue; } this.nodeMap_[path]._children.sort(sortFunction); } this.root_ = this.nodeMap_['']; this.root_.x0 = 0; this.root_.y0 = 0; this.toggle_(this.root_); this.update_(this.root_); }; /** * Builds all the nodes in the tree. */ ImageFileChangeTree.prototype.buildNodes_ = function(path) { var parts = path.split('/'); for (var i = 0; i < parts.length; ++i) { var currentPath = parts.slice(0, i + 1).join('/'); if (!this.nodeMap_[currentPath]) { this.nodeMap_[currentPath] = { 'name': parts[i] || '/', 'path': currentPath, '_children': [] }; if (currentPath.length > 0) { var parentPath = parts.slice(0, i).join('/'); this.nodeMap_[parentPath]._children.push(this.nodeMap_[currentPath]); } } } return this.nodeMap_[path]; }; /** * Calculates the count of visible nodes. */ ImageFileChangeTree.prototype.getVisibleCount_ = function(node) { if (node.children) { var count = 1; for (var i = 0; i < node.children.length; ++i) { count += this.getVisibleCount_(node.children[i]); } return count; } return 1; }; /** * Calculates the height for the container. */ ImageFileChangeTree.prototype.getContainerHeight_ = function() { var dimensions = this.calculateDimensions_(this.container_); var barHeight = this.barHeight_; var height = (this.getVisibleCount_(this.root_) * (barHeight + 2)); return height + dimensions.m[0] + dimensions.m[2]; }; /** * Updates the tree starting at the given source node. */ ImageFileChangeTree.prototype.update_ = function(source) { var that = this; var tree = this.tree_; var vis = this.vis_; var svg = this.rootSvg_; var diagonal = this.diagonal_; var barWidth = this.barWidth_; var barHeight = this.barHeight_; var duration = 400; var color = function(d) { if (d.kind) { return ''; } return d._children ? "#E9E9E9" : "#c6dbef"; }; // Update the height of the container and the SVG. document.getElementById(this.container_).style.height = this.getContainerHeight_() + 'px'; svg.attr('height', this.getContainerHeight_()); // Compute the flattened node list. var nodes = tree.nodes(this.root_); // Compute the "layout". nodes.forEach(function(n, i) { n.x = i * barHeight; }); // Update the nodes... var node = vis.selectAll("g.node") .data(nodes, function(d) { return d.id || (d.id = that.idCounter_++); }); var nodeEnter = node.enter().append("svg:g") .attr("class", function(d) { return "node " + (d.kind ? d.kind : 'folder'); }) .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; }) .style("opacity", 1e-6); // Enter any new nodes at the parent's previous position. nodeEnter.append("svg:rect") .attr("class", "main-rect") .attr("y", -barHeight / 2) .attr("height", barHeight) .attr("width", barWidth) .style("fill", color) .on("click", function(d) { that.toggle_(d); that.update_(source); }); nodeEnter.append("svg:text") .attr("dy", 3.5) .attr("dx", 5.5 + 18) .text(function(d) { return d.name; }); var body = nodeEnter.append('svg:foreignObject') .attr("class", "fo") .attr("width", 18) .attr("height", barHeight) .append('xhtml:body'); body.append('div') .attr('class', 'node-icon'); // Transition nodes to their new position. nodeEnter.transition() .duration(duration) .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; }) .style("opacity", 1); node.transition() .duration(duration) // TODO: reenable for full animation //.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; }) .style("opacity", 1) .select("rect") .style("fill", color); // TODO: remove if full animation. node.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; }); node.select('.main-rect') .attr("y", -barHeight / 2) .attr("height", barHeight) .attr("width", barWidth) node.select('.fo') .attr("x", function(d) { return d.kind ? barWidth - 18 : 0; }) .attr("y", -10) node.select('.node-icon') .html(function(d) { if (!d.kind) { var folder = d._children ? 'fa fa-folder' : 'fa fa-folder-open'; return ''; } var icon = { 'added': 'plus-square', 'removed': 'minus-square', 'changed': 'pencil-square' }; return ''; }); // Transition exiting nodes to the parent's new position. node.exit().transition() .duration(duration) // TODO: reenable for full animation // .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; }) .style("opacity", 1e-6) .remove(); // 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", "link") .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", function(d) { var s = {x: d.source.x + 14, y: d.source.y + 9}; var t = d.target; return diagonal({source: s, target: t}); }); // 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. */ ImageFileChangeTree.prototype.toggle_ = function(d) { if (d.children) { d._children = d.children; d.children = null; } else { d.children = d._children; d._children = null; } };