fe69ba5ec1
- Have conversion to organization update its plan to a business plan - Fix bug in the repo donut usage graph thingy where it had zero size when not in the default tab
1245 lines
No EOL
33 KiB
JavaScript
1245 lines
No EOL
33 KiB
JavaScript
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 += '<span>' + d.encountered[i].image.id.substr(0, 12) + '</span>';
|
|
html += '<span class="created">' + formatTime(d.encountered[i].image.created) + '</span>';
|
|
}
|
|
return html;
|
|
}
|
|
|
|
if (!d.image) {
|
|
return '(This repository is empty)';
|
|
}
|
|
|
|
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_();
|
|
|
|
// 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 += '<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);
|
|
|
|
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 '<i class="' + folder + '"></i>';
|
|
}
|
|
|
|
var icon = {
|
|
'added': 'plus-square',
|
|
'removed': 'minus-square',
|
|
'changed': 'pencil-square'
|
|
};
|
|
|
|
return '<i class="change-icon fa fa-' + 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", 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;
|
|
}
|
|
};
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* Based off of http://bl.ocks.org/mbostock/1346410
|
|
*/
|
|
function RepositoryUsageChart() {
|
|
this.total_ = null;
|
|
this.count_ = null;
|
|
this.drawn_ = false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Updates the chart with the given count and total of number of repositories.
|
|
*/
|
|
RepositoryUsageChart.prototype.update = function(count, total) {
|
|
if (!this.g_) { return; }
|
|
this.total_ = total;
|
|
this.count_ = count;
|
|
this.drawInternal_();
|
|
};
|
|
|
|
|
|
/**
|
|
* Conducts the actual draw or update (if applicable).
|
|
*/
|
|
RepositoryUsageChart.prototype.drawInternal_ = function() {
|
|
// If the total is null, then we have not yet set the proper counts.
|
|
if (this.total_ === null) { return; }
|
|
|
|
var duration = 750;
|
|
|
|
var arc = this.arc_;
|
|
var pie = this.pie_;
|
|
var arcTween = this.arcTween_;
|
|
|
|
var color = d3.scale.category20();
|
|
var count = this.count_;
|
|
var total = this.total_;
|
|
|
|
var data = [Math.max(count, 1), Math.max(0, total - count)];
|
|
|
|
var arcTween = function(a) {
|
|
var i = d3.interpolate(this._current, a);
|
|
this._current = i(0);
|
|
return function(t) {
|
|
return arc(i(t));
|
|
};
|
|
};
|
|
|
|
if (!this.drawn_) {
|
|
var text = this.g_.append("svg:text")
|
|
.attr("dy", 10)
|
|
.attr("dx", 0)
|
|
.attr('dominant-baseline', 'auto')
|
|
.attr('text-anchor', 'middle')
|
|
.attr('class', 'count-text')
|
|
.text(this.count_ + ' / ' + this.total_);
|
|
|
|
var path = this.g_.datum(data).selectAll("path")
|
|
.data(pie)
|
|
.enter().append("path")
|
|
.attr("fill", function(d, i) { return color(i); })
|
|
.attr("class", function(d, i) { return 'arc-' + i; })
|
|
.attr("d", arc)
|
|
.each(function(d) { this._current = d; }); // store the initial angles
|
|
|
|
this.path_ = path;
|
|
this.text_ = text;
|
|
} else {
|
|
pie.value(function(d, i) { return data[i]; }); // change the value function
|
|
this.path_ = this.path_.data(pie); // compute the new angles
|
|
this.path_.transition().duration(duration).attrTween("d", arcTween); // redraw the arcs
|
|
|
|
// Update the text.
|
|
this.text_.text(this.count_ + ' / ' + this.total_);
|
|
}
|
|
|
|
this.drawn_ = true;
|
|
};
|
|
|
|
|
|
/**
|
|
* Draws the chart in the given container.
|
|
*/
|
|
RepositoryUsageChart.prototype.draw = function(container) {
|
|
var cw = 200;
|
|
var ch = 200;
|
|
var radius = Math.min(cw, ch) / 2;
|
|
|
|
var pie = d3.layout.pie().sort(null);
|
|
|
|
var arc = d3.svg.arc()
|
|
.innerRadius(radius - 50)
|
|
.outerRadius(radius - 25);
|
|
|
|
var svg = d3.select("#" + container).append("svg:svg")
|
|
.attr("width", cw)
|
|
.attr("height", ch);
|
|
|
|
var g = svg.append("g")
|
|
.attr("transform", "translate(" + cw / 2 + "," + ch / 2 + ")");
|
|
|
|
this.svg_ = svg;
|
|
this.g_ = g;
|
|
this.pie_ = pie;
|
|
this.arc_ = arc;
|
|
this.width_ = cw;
|
|
this.drawInternal_();
|
|
}; |