2013-10-10 06:09:17 +00:00
|
|
|
var DEPTH_HEIGHT = 100;
|
2013-10-11 05:06:04 +00:00
|
|
|
var DEPTH_WIDTH = 132;
|
2013-10-10 06:09:17 +00:00
|
|
|
|
2013-10-10 04:40:18 +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)
|
2013-10-10 04:40:18 +00:00
|
|
|
*/
|
2013-10-17 02:33:35 +00:00
|
|
|
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;
|
|
|
|
|
2013-10-10 04:40:18 +00:00
|
|
|
/**
|
|
|
|
* The images to display.
|
|
|
|
*/
|
|
|
|
this.images_ = images;
|
|
|
|
|
2013-10-10 06:09:17 +00:00
|
|
|
/**
|
2013-10-17 02:33:35 +00:00
|
|
|
* Method to invoke to format a comment for an image.
|
2013-10-10 06:09:17 +00:00
|
|
|
*/
|
2013-10-17 02:33:35 +00:00
|
|
|
this.formatComment_ = formatComment;
|
2013-10-10 06:09:17 +00:00
|
|
|
|
2013-10-10 04:40:18 +00:00
|
|
|
/**
|
2013-10-17 02:33:35 +00:00
|
|
|
* Method to invoke to format the time for an image.
|
2013-10-10 04:40:18 +00:00
|
|
|
*/
|
2013-10-17 02:33:35 +00:00
|
|
|
this.formatTime_ = formatTime;
|
2013-10-10 04:40:18 +00:00
|
|
|
|
|
|
|
/**
|
2013-10-17 02:33:35 +00:00
|
|
|
* The current tag (if any).
|
2013-10-10 04:40:18 +00:00
|
|
|
*/
|
2013-10-17 02:33:35 +00:00
|
|
|
this.currentTag_ = null;
|
2013-10-10 04:40:18 +00:00
|
|
|
|
|
|
|
/**
|
2013-10-17 02:33:35 +00:00
|
|
|
* The current image (if any).
|
2013-10-10 04:40:18 +00:00
|
|
|
*/
|
2013-10-17 02:33:35 +00:00
|
|
|
this.currentImage_ = null;
|
2013-10-10 04:40:18 +00:00
|
|
|
|
|
|
|
/**
|
2013-10-17 02:33:35 +00:00
|
|
|
* Counter for creating unique IDs.
|
2013-10-10 04:40:18 +00:00
|
|
|
*/
|
2013-10-17 02:33:35 +00:00
|
|
|
this.idCounter_ = 0;
|
2013-10-10 04:40:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Draws the tree.
|
|
|
|
*/
|
|
|
|
ImageHistoryTree.prototype.draw = function(container) {
|
2013-10-10 06:09:17 +00:00
|
|
|
// Build the root of the tree.
|
2013-10-11 05:06:04 +00:00
|
|
|
var result = this.buildRoot_();
|
|
|
|
this.maxWidth_ = result['maxWidth'];
|
|
|
|
this.maxHeight_ = result['maxHeight'];
|
2013-10-10 06:09:17 +00:00
|
|
|
|
2013-10-10 21:13:42 +00:00
|
|
|
// Determine the size of the SVG container.
|
2013-10-11 05:06:04 +00:00
|
|
|
var width = Math.max(document.getElementById(container).clientWidth, this.maxWidth_ * DEPTH_WIDTH);
|
|
|
|
var height = this.maxHeight_ * (DEPTH_HEIGHT + 10);
|
2013-10-10 04:40:18 +00:00
|
|
|
|
2013-10-11 05:06:04 +00:00
|
|
|
// Set the height of the container so that it never goes offscreen.
|
|
|
|
var viewportHeight = $(window).height();
|
|
|
|
var boundingBox = document.getElementById(container).getBoundingClientRect();
|
|
|
|
document.getElementById(container).style.maxHeight = (viewportHeight - boundingBox.top) + 'px';
|
|
|
|
|
2013-10-12 01:28:02 +00:00
|
|
|
var margin = { top: 40, right: 20, bottom: 20, left: 40 };
|
2013-10-10 04:40:18 +00:00
|
|
|
var m = [margin.top, margin.right, margin.bottom, margin.left];
|
|
|
|
var w = width - m[1] - m[3];
|
|
|
|
var h = height - m[0] - m[2];
|
2013-10-10 06:09:17 +00:00
|
|
|
|
2013-10-10 21:13:42 +00:00
|
|
|
// Create the tree and all its components.
|
2013-10-10 04:40:18 +00:00
|
|
|
var tree = d3.layout.tree()
|
2013-10-11 05:06:04 +00:00
|
|
|
.separation(function() { return 2; })
|
2013-10-11 00:43:37 +00:00
|
|
|
.size([w, h]);
|
2013-10-10 04:40:18 +00:00
|
|
|
|
|
|
|
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])
|
2013-10-11 19:02:08 +00:00
|
|
|
.attr("class", "image-tree")
|
2013-10-10 04:40:18 +00:00
|
|
|
.append("svg:g")
|
2013-10-11 19:02:08 +00:00
|
|
|
.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
|
2013-10-10 04:40:18 +00:00
|
|
|
|
|
|
|
var formatComment = this.formatComment_;
|
|
|
|
var formatTime = this.formatTime_;
|
|
|
|
var tip = d3.tip()
|
|
|
|
.attr('class', 'd3-tip')
|
2013-10-10 21:13:42 +00:00
|
|
|
.offset([-1, 24])
|
|
|
|
.direction('e')
|
2013-10-10 04:40:18 +00:00
|
|
|
.html(function(d) {
|
|
|
|
var html = '';
|
2013-10-12 01:28:02 +00:00
|
|
|
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-11 05:06:04 +00:00
|
|
|
|
2013-10-10 04:40:18 +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);
|
|
|
|
|
2013-10-10 21:13:42 +00:00
|
|
|
// Save all the state created.
|
2013-10-10 06:09:17 +00:00
|
|
|
this.fullWidth_ = width;
|
|
|
|
|
2013-10-10 04:40:18 +00:00
|
|
|
this.width_ = w;
|
|
|
|
this.height_ = h;
|
|
|
|
|
|
|
|
this.diagonal_ = diagonal;
|
|
|
|
this.vis_ = vis;
|
|
|
|
this.tip_ = tip;
|
|
|
|
|
|
|
|
this.tree_ = tree;
|
|
|
|
|
2013-10-10 06:09:17 +00:00
|
|
|
// Populate the tree.
|
2013-10-11 05:06:04 +00:00
|
|
|
this.root_.x0 = this.fullWidth_ / 2;
|
|
|
|
this.root_.y0 = 0;
|
|
|
|
this.setTag_(this.currentTag_);
|
|
|
|
|
|
|
|
$('#' + container).overscroll();
|
2013-10-10 04:40:18 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
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 04:40:18 +00:00
|
|
|
/**
|
2013-10-10 06:09:17 +00:00
|
|
|
* Returns the ancestors of the given image.
|
2013-10-10 04:40:18 +00:00
|
|
|
*/
|
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 04:40:18 +00:00
|
|
|
|
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() {
|
2013-10-10 04:40:18 +00:00
|
|
|
// 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,
|
2013-10-10 06:09:17 +00:00
|
|
|
"tags": image.tags
|
2013-10-10 04:40:18 +00:00
|
|
|
};
|
|
|
|
imageByDBID[image.dbid] = imageNode;
|
|
|
|
}
|
2013-10-10 21:13:42 +00:00
|
|
|
this.imageByDBID_ = imageByDBID;
|
|
|
|
|
2013-10-10 04:40:18 +00:00
|
|
|
// 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;
|
2013-10-10 04:40:18 +00:00
|
|
|
var parent = imageByDBID[immediateParent];
|
2013-10-11 05:06:04 +00:00
|
|
|
if (parent) {
|
2013-10-12 01:28:02 +00:00
|
|
|
// Add a reference to the parent. This makes walking the tree later easier.
|
|
|
|
imageNode.parent = parent;
|
|
|
|
parent.children.push(imageNode);
|
2013-10-10 04:40:18 +00:00
|
|
|
} else {
|
|
|
|
formatted = imageNode;
|
|
|
|
}
|
2013-10-11 05:06:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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) {
|
2013-10-12 01:28:02 +00:00
|
|
|
var image = this.images_[i];
|
2013-10-11 05:06:04 +00:00
|
|
|
var imageNode = imageByDBID[image.dbid];
|
2013-10-12 01:28:02 +00:00
|
|
|
maxChildCount = Math.max(maxChildCount, this.determineMaximumChildCount_(imageNode));
|
2013-10-11 05:06:04 +00:00
|
|
|
}
|
2013-10-10 04:40:18 +00:00
|
|
|
|
2013-10-11 05:06:04 +00:00
|
|
|
// 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) {
|
2013-10-12 01:28:02 +00:00
|
|
|
this.collapseNodes_(formatted);
|
2013-10-10 06:09:17 +00:00
|
|
|
}
|
2013-10-10 04:40:18 +00:00
|
|
|
|
2013-10-11 05:06:04 +00:00
|
|
|
// Determine the maximum height of the tree.
|
|
|
|
var maxHeight = this.determineMaximumHeight_(formatted);
|
|
|
|
|
|
|
|
// Finally, set the root node and return.
|
2013-10-10 04:40:18 +00:00
|
|
|
this.root_ = formatted;
|
2013-10-11 05:06:04 +00:00
|
|
|
|
|
|
|
return {
|
2013-10-12 01:28:02 +00:00
|
|
|
'maxWidth': maxChildCount + 1,
|
|
|
|
'maxHeight': maxHeight
|
2013-10-11 05:06:04 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
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) {
|
2013-10-12 01:28:02 +00:00
|
|
|
// 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 + ' images)',
|
|
|
|
"children": [current],
|
|
|
|
"collapsed": true,
|
|
|
|
"encountered": encountered
|
|
|
|
};
|
|
|
|
node.children = [collapsed];
|
|
|
|
|
|
|
|
// Update the parent relationships.
|
|
|
|
collapsed.parent = node;
|
|
|
|
current.parent = collapsed;
|
|
|
|
return;
|
|
|
|
}
|
2013-10-11 05:06:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for (var i = 0; i < node.children.length; ++i) {
|
2013-10-12 01:28:02 +00:00
|
|
|
this.collapseNodes_(node.children[i]);
|
2013-10-11 05:06:04 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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) {
|
2013-10-12 01:28:02 +00:00
|
|
|
nestedCount += children[i].children.length;
|
2013-10-11 05:06:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2013-10-12 01:28:02 +00:00
|
|
|
var image = this.images_[i];
|
|
|
|
if (checker(image)) {
|
|
|
|
return image;
|
|
|
|
}
|
2013-10-11 00:43:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2013-10-11 05:06:04 +00:00
|
|
|
/**
|
|
|
|
* 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) {
|
2013-10-12 01:28:02 +00:00
|
|
|
currentNode.highlighted = isHighlighted;
|
|
|
|
currentNode = currentNode.parent;
|
2013-10-11 05:06:04 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2013-10-10 21:13:42 +00:00
|
|
|
/**
|
|
|
|
* Sets the current tag displayed in the tree.
|
|
|
|
*/
|
|
|
|
ImageHistoryTree.prototype.setTag_ = function(tagName) {
|
2013-10-17 02:33:35 +00:00
|
|
|
// Save the current tag.
|
2013-10-10 21:13:42 +00:00
|
|
|
this.currentTag_ = tagName;
|
2013-10-11 05:06:04 +00:00
|
|
|
|
|
|
|
var imageByDBID = this.imageByDBID_;
|
2013-10-10 21:13:42 +00:00
|
|
|
|
2013-10-17 02:33:35 +00:00
|
|
|
// Update the state of each existing node to no longer be highlighted.
|
|
|
|
if (this.currentImage_) {
|
|
|
|
var currentNode = imageByDBID[this.currentImage_.dbid];
|
|
|
|
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) {
|
2013-10-17 02:33:35 +00:00
|
|
|
return image.tags.indexOf(tagName || '(no tag specified)') >= 0;
|
2013-10-11 00:43:37 +00:00
|
|
|
});
|
2013-10-10 21:13:42 +00:00
|
|
|
|
2013-10-11 05:06:04 +00:00
|
|
|
// Update the state of the new node path.
|
2013-10-17 02:33:35 +00:00
|
|
|
if (this.currentImage_) {
|
|
|
|
var currentNode = imageByDBID[this.currentImage_.dbid];
|
|
|
|
this.markPath_(currentNode, true);
|
|
|
|
}
|
2013-10-10 21:13:42 +00:00
|
|
|
|
|
|
|
// Ensure that the children are in the correct order.
|
|
|
|
for (var i = 0; i < this.images_.length; ++i) {
|
2013-10-12 01:28:02 +00:00
|
|
|
var image = this.images_[i];
|
|
|
|
var imageNode = this.imageByDBID_[image.dbid];
|
|
|
|
var ancestors = this.getAncestors_(image);
|
2013-10-10 21:13:42 +00:00
|
|
|
var immediateParent = ancestors[ancestors.length - 1] * 1;
|
|
|
|
var parent = imageByDBID[immediateParent];
|
2013-10-12 01:28:02 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2013-10-10 21:13:42 +00:00
|
|
|
}
|
|
|
|
|
2013-10-11 05:06:04 +00:00
|
|
|
// 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) {
|
2013-10-12 01:28:02 +00:00
|
|
|
return image.id == imageId;
|
2013-10-11 00:43:37 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
if (newImage == this.currentImage_) {
|
2013-10-12 01:28:02 +00:00
|
|
|
return;
|
2013-10-11 00:43:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
this.currentImage_ = newImage;
|
2013-10-10 21:13:42 +00:00
|
|
|
this.update_(this.root_);
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2013-10-10 04:40:18 +00:00
|
|
|
/**
|
|
|
|
* 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_;
|
2013-10-17 02:33:35 +00:00
|
|
|
var currentImage = this.currentImage_;
|
2013-10-10 06:09:17 +00:00
|
|
|
var repoNamespace = this.repoNamespace_;
|
|
|
|
var repoName = this.repoName_;
|
2013-10-10 21:13:42 +00:00
|
|
|
var maxHeight = this.maxHeight_;
|
2013-10-10 04:40:18 +00:00
|
|
|
|
|
|
|
var that = this;
|
|
|
|
|
|
|
|
var duration = 500;
|
|
|
|
|
|
|
|
// Compute the new tree layout.
|
|
|
|
var nodes = tree.nodes(this.root_).reverse();
|
|
|
|
|
|
|
|
// Normalize for fixed-depth.
|
2013-10-10 21:13:42 +00:00
|
|
|
nodes.forEach(function(d) { d.y = (maxHeight - d.depth - 1) * DEPTH_HEIGHT; });
|
2013-10-10 04:40:18 +00:00
|
|
|
|
|
|
|
// 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")
|
2013-10-10 21:13:42 +00:00
|
|
|
.attr("class", "node")
|
2013-10-10 06:09:17 +00:00
|
|
|
.attr("transform", function(d) { return "translate(" + source.x0 + "," + source.y0 + ")"; });
|
2013-10-10 04:40:18 +00:00
|
|
|
|
|
|
|
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 04:40:18 +00:00
|
|
|
|
2013-10-10 06:09:17 +00:00
|
|
|
// Add the repo ID.
|
|
|
|
g.append("svg:text")
|
2013-10-10 04:40:18 +00:00
|
|
|
.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-11 00:43:37 +00:00
|
|
|
.on("click", function(d) { that.changeImage_(d.image.id); })
|
2013-10-10 04:40:18 +00:00
|
|
|
.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")
|
2013-10-10 21:13:42 +00:00
|
|
|
.attr("class", "fo")
|
2013-10-10 06:09:17 +00:00
|
|
|
.attr("x", 14)
|
2013-10-10 21:13:42 +00:00
|
|
|
.attr("y", 12)
|
2013-10-10 06:09:17 +00:00
|
|
|
.attr("width", 110)
|
|
|
|
.attr("height", DEPTH_HEIGHT - 20);
|
|
|
|
|
2013-10-10 21:13:42 +00:00
|
|
|
// Add the tags container.
|
2013-10-10 06:09:17 +00:00
|
|
|
fo.append('xhtml:div')
|
|
|
|
.attr("class", "tags")
|
2013-10-10 21:13:42 +00:00
|
|
|
.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 ] + ")";
|
|
|
|
});
|
|
|
|
|
2013-10-10 04:40:18 +00:00
|
|
|
// Transition nodes to their new position.
|
|
|
|
var nodeUpdate = node.transition()
|
|
|
|
.duration(duration)
|
|
|
|
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
|
|
|
|
|
2013-10-10 21:13:42 +00:00
|
|
|
// Update the node circle.
|
2013-10-10 04:40:18 +00:00
|
|
|
nodeUpdate.select("circle")
|
|
|
|
.attr("r", 4.5)
|
2013-10-10 21:13:42 +00:00
|
|
|
.attr("class", function(d) {
|
2013-10-12 01:28:02 +00:00
|
|
|
return (d._children ? "closed " : "open ") + (d.current ? "current " : "") + (d.highlighted ? "highlighted " : "");
|
2013-10-10 21:13:42 +00:00
|
|
|
})
|
2013-10-10 04:40:18 +00:00
|
|
|
.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) {
|
2013-10-12 01:28:02 +00:00
|
|
|
if (d.collapsed) {
|
|
|
|
return 'collapsed';
|
|
|
|
}
|
2013-10-17 02:33:35 +00:00
|
|
|
if (!currentImage) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
return d.image.id == currentImage.id ? 'current' : '';
|
2013-10-11 00:43:37 +00:00
|
|
|
});
|
|
|
|
|
2013-10-10 21:13:42 +00:00
|
|
|
// Ensure that the node is visible.
|
2013-10-10 06:09:17 +00:00
|
|
|
nodeUpdate.select("g")
|
2013-10-10 04:40:18 +00:00
|
|
|
.style("fill-opacity", 1);
|
|
|
|
|
2013-10-10 21:13:42 +00:00
|
|
|
// Update the tags.
|
|
|
|
node.select(".tags")
|
|
|
|
.html(function(d) {
|
2013-10-12 01:28:02 +00:00
|
|
|
if (!d.tags) {
|
|
|
|
return '';
|
|
|
|
}
|
2013-10-11 05:06:04 +00:00
|
|
|
|
2013-10-10 21:13:42 +00:00
|
|
|
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) {
|
2013-10-12 01:28:02 +00:00
|
|
|
var tag = this.getAttribute('data-tag');
|
|
|
|
if (tag) {
|
|
|
|
that.changeTag_(tag);
|
|
|
|
}
|
2013-10-10 21:13:42 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// 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) {
|
2013-10-12 01:28:02 +00:00
|
|
|
return DEPTH_HEIGHT - 20 + Math.random() / 10;
|
2013-10-10 21:13:42 +00:00
|
|
|
});
|
|
|
|
|
2013-10-10 04:40:18 +00:00
|
|
|
// 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")
|
2013-10-10 04:40:18 +00:00
|
|
|
.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)
|
2013-10-10 21:13:42 +00:00
|
|
|
.attr("d", diagonal)
|
|
|
|
.attr("class", function(d) {
|
|
|
|
var isHighlighted = d.target.highlighted;
|
|
|
|
return "link " + (isHighlighted ? "highlighted": "");
|
|
|
|
});
|
2013-10-10 04:40:18 +00:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
};
|