This repository has been archived on 2020-03-24. You can view files and clone it, but cannot push or open issues or pull requests.
quay/static/js/graphing.js

1062 lines
28 KiB
JavaScript
Raw Normal View History

2013-10-10 06:09:17 +00:00
var DEPTH_HEIGHT = 100;
var DEPTH_WIDTH = 132;
2013-10-10 06:09:17 +00:00
/**
2013-10-10 06:09:17 +00:00
* Based off of http://mbostock.github.io/d3/talk/20111018/tree.html by Mike Bostock (@mbostock)
*/
function ImageHistoryTree(namespace, name, images, formatComment, formatTime) {
2013-10-10 06:09:17 +00:00
/**
* The namespace of the repo.
*/
this.repoNamespace_ = namespace;
/**
* The name of the repo.
*/
this.repoName_ = name;
/**
* The images to display.
*/
this.images_ = images;
2013-10-10 06:09:17 +00:00
/**
* Method to invoke to format a comment for an image.
2013-10-10 06:09:17 +00:00
*/
this.formatComment_ = formatComment;
2013-10-10 06:09:17 +00:00
/**
* 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
};
};
2013-10-10 06:09:17 +00:00
/**
* 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]);
2013-10-10 06:09:17 +00:00
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 += '<span>' + d.encountered[i].image.id.substr(0, 12) + '</span>';
html += '<span class="created">' + formatTime(d.encountered[i].image.created) + '</span>';
}
return html;
}
2013-10-17 02:42:35 +00:00
if (!d.image) {
return '(This repository is empty)';
2013-10-17 02:42:35 +00:00
}
if (d.image.comment) {
html += '<span class="comment">' + formatComment(d.image.comment) + '</span>';
}
html += '<span class="created">' + formatTime(d.image.created) + '</span>';
html += '<span class="full-id">' + d.image.id + '</span>';
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_();
2013-10-10 06:09:17 +00:00
// 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_);
};
2013-10-11 00:43:37 +00:00
/**
* 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);
};
/**
2013-10-10 06:09:17 +00:00
* Returns the ancestors of the given image.
*/
2013-10-11 00:43:37 +00:00
ImageHistoryTree.prototype.getAncestors_ = function(image) {
2013-10-10 06:09:17 +00:00
var ancestorsString = image.ancestors;
// Remove the starting and ending /s.
ancestorsString = ancestorsString.substr(1, ancestorsString.length - 2);
2013-10-10 06:09:17 +00:00
// Split based on /.
ancestors = ancestorsString.split('/');
return ancestors;
};
2013-10-11 00:43:37 +00:00
/**
* 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);
};
2013-10-10 06:09:17 +00:00
/**
* 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": [...]
// }
2013-10-17 02:42:35 +00:00
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,
2013-10-10 06:09:17 +00:00
"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];
2013-10-11 00:43:37 +00:00
var ancestors = this.getAncestors_(image);
2013-10-10 06:09:17 +00:00
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);
2013-10-10 06:09:17 +00:00
}
// 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;
2013-10-17 02:42:35 +00:00
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);
2013-10-10 06:09:17 +00:00
};
2013-10-11 00:43:37 +00:00
/**
* 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;
}
2013-10-11 00:43:37 +00:00
}
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];
2013-10-17 02:44:29 +00:00
this.markPath_(currentNode, false);
}
// Find the new current image (if any).
2013-10-11 00:43:37 +00:00
this.currentImage_ = this.findImage_(function(image) {
return image.tags.indexOf(tagName || '(no tag specified)') >= 0;
2013-10-11 00:43:37 +00:00
});
// Update the state of the new node path.
if (this.currentImage_) {
2013-10-17 02:44:29 +00:00
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.
2013-10-11 00:43:37 +00:00
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;
2013-10-11 00:43:37 +00:00
});
if (newImage == this.currentImage_) {
return;
2013-10-11 00:43:37 +00:00
}
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_;
2013-10-10 06:09:17 +00:00
var currentTag = this.currentTag_;
var currentImage = this.currentImage_;
2013-10-10 06:09:17 +00:00
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")
2013-10-10 06:09:17 +00:00
.attr("transform", function(d) { return "translate(" + source.x0 + "," + source.y0 + ")"; });
nodeEnter.append("svg:circle")
.attr("r", 1e-6)
2013-10-10 06:09:17 +00:00
.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);
2013-10-10 06:09:17 +00:00
// 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; })
2013-10-17 02:42:35 +00:00
.on("click", function(d) { if (d.image) { that.changeImage_(d.image.id); } })
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
2013-10-10 06:09:17 +00:00
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")
2013-10-10 06:09:17 +00:00
.attr("x", 14)
.attr("y", 12)
2013-10-10 06:09:17 +00:00
.attr("width", 110)
.attr("height", DEPTH_HEIGHT - 20);
// Add the tags container.
2013-10-10 06:09:17 +00:00
fo.append('xhtml:div')
.attr("class", "tags")
.style("display", "none");
2013-10-10 06:09:17 +00:00
// 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";
});
2013-10-11 00:43:37 +00:00
// 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' : '';
2013-10-11 00:43:37 +00:00
});
// Ensure that the node is visible.
2013-10-10 06:09:17 +00:00
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 += '<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.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);
2013-10-10 06:09:17 +00:00
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;
}
};
/////////////////////////////////////////////////////////////////////////////////////////
/**
* 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 barWidth = cw * 0.8;
var barHeight = 20;
var ch = (this.changes_.length * barHeight) + 40;
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,
'bw': barWidth,
'bh': barHeight
};
};
/**
* 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("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("x", function(d) { return d.kind ? barWidth - 18 : 0; })
.attr("y", -10)
.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('.node-icon')
.html(function(d) {
if (!d.kind) {
var folder = d._children ? 'icon-folder-close' : 'icon-folder-open';
return '<i class="' + folder + '"></i>';
}
var icon = {
'added': 'plus-sign-alt',
'removed': 'minus-sign-alt',
'changed': 'edit-sign'
};
return '<i class="change-icon icon-' + icon[d.kind] + '"></i>';
});
// 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", 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.
*/
ImageFileChangeTree.prototype.toggle_ = function(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
};