2014-01-10 01:22:22 +00:00
|
|
|
/**
|
|
|
|
* Bind polyfill from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#Compatibility
|
|
|
|
*/
|
2014-02-17 22:28:20 +00:00
|
|
|
if (!Function.prototype.bind) {
|
|
|
|
Function.prototype.bind = function (oThis) {
|
|
|
|
if (typeof this !== "function") {
|
|
|
|
// closest thing possible to the ECMAScript 5 internal IsCallable function
|
|
|
|
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
|
|
|
|
}
|
2014-01-10 01:22:22 +00:00
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
var aArgs = Array.prototype.slice.call(arguments, 1),
|
|
|
|
fToBind = this,
|
|
|
|
fNOP = function () {},
|
|
|
|
fBound = function () {
|
|
|
|
return fToBind.apply(this instanceof fNOP && oThis
|
|
|
|
? this
|
|
|
|
: oThis,
|
|
|
|
aArgs.concat(Array.prototype.slice.call(arguments)));
|
|
|
|
};
|
2014-01-10 01:22:22 +00:00
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
fNOP.prototype = this.prototype;
|
|
|
|
fBound.prototype = new fNOP();
|
2014-01-10 01:22:22 +00:00
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
return fBound;
|
|
|
|
};
|
|
|
|
}
|
2014-01-10 01:22:22 +00:00
|
|
|
|
2013-10-10 06:09:17 +00:00
|
|
|
var DEPTH_HEIGHT = 100;
|
2014-01-20 22:38:33 +00:00
|
|
|
var DEPTH_WIDTH = 140;
|
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
|
|
|
*/
|
2014-01-14 20:19:47 +00:00
|
|
|
function ImageHistoryTree(namespace, name, images, formatComment, formatTime, formatCommand) {
|
2014-02-17 22:28:20 +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;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Method to invoke to format the command for an image.
|
|
|
|
*/
|
|
|
|
this.formatCommand_ = formatCommand;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The current tag (if any).
|
|
|
|
*/
|
|
|
|
this.currentTag_ = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The current image (if any).
|
|
|
|
*/
|
|
|
|
this.currentImage_ = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The currently highlighted node (if any).
|
|
|
|
*/
|
|
|
|
this.currentNode_ = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Counter for creating unique IDs.
|
|
|
|
*/
|
|
|
|
this.idCounter_ = 0;
|
2013-10-10 04:40:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2013-10-17 19:20:27 +00:00
|
|
|
* Calculates the dimensions of the tree.
|
2013-10-10 04:40:18 +00:00
|
|
|
*/
|
2013-10-17 19:20:27 +00:00
|
|
|
ImageHistoryTree.prototype.calculateDimensions_ = function(container) {
|
2014-02-17 22:28:20 +00:00
|
|
|
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: 80 };
|
|
|
|
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-17 19:20:27 +00:00
|
|
|
};
|
2013-10-10 06:09:17 +00:00
|
|
|
|
2013-10-17 19:20:27 +00:00
|
|
|
|
2014-01-06 20:20:58 +00:00
|
|
|
ImageHistoryTree.prototype.setupOverscroll_ = function() {
|
|
|
|
var container = this.container_;
|
|
|
|
var that = this;
|
|
|
|
var overscroll = $('#' + container).overscroll();
|
|
|
|
|
|
|
|
overscroll.on('overscroll:dragstart', function() {
|
|
|
|
$(that).trigger({
|
|
|
|
'type': 'hideTagMenu'
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
overscroll.on('scroll', function() {
|
|
|
|
$(that).trigger({
|
|
|
|
'type': 'hideTagMenu'
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2013-10-17 19:20:27 +00:00
|
|
|
/**
|
|
|
|
* Updates the dimensions of the tree.
|
|
|
|
*/
|
|
|
|
ImageHistoryTree.prototype.updateDimensions_ = function() {
|
2014-02-17 22:28:20 +00:00
|
|
|
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 - 150) + 'px';
|
|
|
|
|
|
|
|
this.setupOverscroll_();
|
|
|
|
|
|
|
|
// Update the tree.
|
|
|
|
var rootSvg = this.rootSvg_;
|
|
|
|
var tree = this.tree_;
|
|
|
|
var vis = this.vis_;
|
|
|
|
|
|
|
|
|
|
|
|
var ow = w + m[1] + m[3];
|
|
|
|
var oh = h + m[0] + m[2];
|
|
|
|
rootSvg
|
|
|
|
.attr("width", ow)
|
|
|
|
.attr("height", oh)
|
|
|
|
.attr("style", "width: " + ow + "px; height: " + oh + "px");
|
|
|
|
|
|
|
|
tree.size([w, h]);
|
|
|
|
vis.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
|
|
|
|
|
|
|
|
return dimensions;
|
2013-10-17 19:20:27 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Draws the tree.
|
|
|
|
*/
|
|
|
|
ImageHistoryTree.prototype.draw = function(container) {
|
2014-02-17 22:28:20 +00:00
|
|
|
// 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 formatCommand = this.formatCommand_;
|
|
|
|
|
|
|
|
var tip = d3.tip()
|
|
|
|
.attr('class', 'd3-tip')
|
|
|
|
.offset([-1, 24])
|
|
|
|
.direction('e')
|
|
|
|
.html(function(d) {
|
|
|
|
var html = '';
|
|
|
|
if (d.virtual) {
|
|
|
|
return d.name;
|
|
|
|
}
|
|
|
|
|
|
|
|
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>';
|
|
|
|
}
|
|
|
|
if (d.image.command && d.image.command.length) {
|
|
|
|
html += '<span class="command info-line"><i class="fa fa-terminal"></i>' + formatCommand(d.image) + '</span>';
|
|
|
|
}
|
2014-04-07 23:53:25 +00:00
|
|
|
html += '<span class="created info-line"><i class="fa fa-calendar"></i>' + formatTime(d.image.created) + '</span>';
|
|
|
|
|
|
|
|
var tags = d.tags || [];
|
|
|
|
html += '<span class="tooltip-tags tags">';
|
|
|
|
for (var i = 0; i < tags.length; ++i) {
|
|
|
|
var tag = tags[i];
|
|
|
|
var kind = 'default';
|
|
|
|
html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '">' + tag + '</span>';
|
|
|
|
}
|
|
|
|
html += '</span>';
|
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
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_);
|
|
|
|
this.setupOverscroll_();
|
2013-10-10 04:40:18 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
2013-10-17 18:29:47 +00:00
|
|
|
/**
|
|
|
|
* Redraws the image history to fit the new size.
|
|
|
|
*/
|
|
|
|
ImageHistoryTree.prototype.notifyResized = function() {
|
2013-10-17 19:20:27 +00:00
|
|
|
this.updateDimensions_();
|
|
|
|
this.update_(this.root_);
|
2013-10-17 18:29:47 +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);
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2014-01-09 23:54:59 +00:00
|
|
|
/**
|
|
|
|
* Updates the highlighted path in the tree.
|
|
|
|
*/
|
|
|
|
ImageHistoryTree.prototype.setHighlightedPath_ = function(image) {
|
|
|
|
if (this.currentNode_) {
|
|
|
|
this.markPath_(this.currentNode_, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
var imageByDBID = this.imageByDBID_;
|
|
|
|
var currentNode = imageByDBID[image.dbid];
|
|
|
|
if (currentNode) {
|
|
|
|
this.markPath_(currentNode, true);
|
|
|
|
this.currentNode_ = currentNode;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
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({
|
2014-02-17 22:28:20 +00:00
|
|
|
'type': 'tagChanged',
|
|
|
|
'tag': tagName
|
2013-10-11 00:43:37 +00:00
|
|
|
});
|
|
|
|
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({
|
2014-02-17 22:28:20 +00:00
|
|
|
'type': 'imageChanged',
|
|
|
|
'image': this.findImage_(function(image) { return image.id == imageId; })
|
2013-10-11 00:43:37 +00:00
|
|
|
});
|
|
|
|
this.setImage_(imageId);
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2014-04-08 00:11:41 +00:00
|
|
|
/**
|
|
|
|
* Expands the given collapsed node in the tree.
|
|
|
|
*/
|
|
|
|
ImageHistoryTree.prototype.expandCollapsed_ = function(imageNode) {
|
|
|
|
var index = imageNode.parent.children.indexOf(imageNode);
|
|
|
|
if (index < 0 || imageNode.encountered.length < 2) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Note: we start at 1 since the 0th encountered node is the parent.
|
|
|
|
imageNode.parent.children.splice(index, 1, imageNode.encountered[1]);
|
|
|
|
this.maxHeight_ = this.determineMaximumHeight_(this.root_);
|
|
|
|
this.update_(this.root_);
|
|
|
|
this.updateDimensions_();
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2013-10-10 06:09:17 +00:00
|
|
|
/**
|
|
|
|
* Builds the root node for the tree.
|
|
|
|
*/
|
|
|
|
ImageHistoryTree.prototype.buildRoot_ = function() {
|
2014-02-17 22:28:20 +00:00
|
|
|
// 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.
|
|
|
|
var roots = [];
|
|
|
|
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 {
|
|
|
|
roots.push(imageNode);
|
2013-10-11 05:06:04 +00:00
|
|
|
}
|
2014-02-17 22:28:20 +00:00
|
|
|
}
|
2013-10-11 05:06:04 +00:00
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
// If there are multiple root nodes, then there is at least one branch without shared
|
|
|
|
// ancestry and we use the virtual node. Otherwise, we use the root node found.
|
|
|
|
var root = {
|
|
|
|
'name': '',
|
|
|
|
'children': roots,
|
|
|
|
'virtual': true
|
|
|
|
};
|
2013-11-08 21:30:18 +00:00
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
if (roots.length == 1) {
|
|
|
|
root = roots[0];
|
|
|
|
}
|
2013-11-08 21:30:18 +00:00
|
|
|
|
2014-02-17 22:28:20 +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 = roots.length;
|
|
|
|
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));
|
|
|
|
}
|
2013-10-10 04:40:18 +00:00
|
|
|
|
2014-02-17 22:28:20 +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) {
|
|
|
|
this.collapseNodes_(root);
|
|
|
|
}
|
2013-10-10 04:40:18 +00:00
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
// Determine the maximum height of the tree.
|
|
|
|
var maxHeight = this.determineMaximumHeight_(root);
|
2013-10-11 05:06:04 +00:00
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
// Finally, set the root node and return.
|
|
|
|
this.root_ = root;
|
2013-10-11 05:06:04 +00:00
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
return {
|
|
|
|
'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) {
|
2014-02-17 22:28:20 +00:00
|
|
|
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;
|
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.collapseNodes_ = function(node) {
|
2014-02-17 22:28:20 +00:00
|
|
|
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];
|
2013-10-11 05:06:04 +00:00
|
|
|
}
|
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
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;
|
2013-10-11 05:06:04 +00:00
|
|
|
}
|
2014-02-17 22:28:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for (var i = 0; i < node.children.length; ++i) {
|
|
|
|
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) {
|
2014-02-17 22:28:20 +00:00
|
|
|
var children = node.children;
|
|
|
|
var myLevelCount = children.length;
|
|
|
|
var nestedCount = 0;
|
2013-10-11 05:06:04 +00:00
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
for (var i = 0; i < children.length; ++i) {
|
|
|
|
nestedCount += children[i].children.length;
|
|
|
|
}
|
2013-10-11 05:06:04 +00:00
|
|
|
|
2014-02-17 22:28:20 +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) {
|
2014-02-17 22:28:20 +00:00
|
|
|
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
|
|
|
}
|
2014-02-17 22:28:20 +00:00
|
|
|
}
|
2013-10-11 00:43:37 +00:00
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
return null;
|
2013-10-11 00:43:37 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
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) {
|
2014-02-17 22:28:20 +00:00
|
|
|
var currentNode = startingNode;
|
|
|
|
currentNode.current = isHighlighted;
|
|
|
|
while (currentNode != null) {
|
|
|
|
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) {
|
2014-02-17 22:28:20 +00:00
|
|
|
if (tagName == this.currentTag_) {
|
|
|
|
return;
|
|
|
|
}
|
2013-10-17 03:55:11 +00:00
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
var imageByDBID = this.imageByDBID_;
|
|
|
|
|
|
|
|
// Save the current tag.
|
|
|
|
var previousTagName = this.currentTag_;
|
|
|
|
this.currentTag_ = tagName;
|
|
|
|
this.currentImage_ = null;
|
|
|
|
|
|
|
|
// Update the path.
|
|
|
|
var tagImage = this.findImage_(function(image) {
|
|
|
|
return image.tags.indexOf(tagName || '(no tag specified)') >= 0;
|
|
|
|
});
|
2013-10-10 21:13:42 +00:00
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
if (tagImage) {
|
|
|
|
this.setHighlightedPath_(tagImage);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
2013-10-10 21:13:42 +00:00
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
if (arr[0] != imageNode) {
|
|
|
|
var index = arr.indexOf(imageNode);
|
|
|
|
if (index > 0) {
|
|
|
|
arr.splice(index, 1);
|
|
|
|
arr.splice(0, 0, imageNode);
|
2013-10-12 01:28:02 +00:00
|
|
|
}
|
2014-02-17 22:28:20 +00:00
|
|
|
}
|
2013-10-10 21:13:42 +00:00
|
|
|
}
|
2014-02-17 22:28:20 +00:00
|
|
|
}
|
2013-10-10 21:13:42 +00:00
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
// Update the tree.
|
|
|
|
this.update_(this.root_);
|
2013-10-11 00:43:37 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the current image highlighted in the tree.
|
|
|
|
*/
|
|
|
|
ImageHistoryTree.prototype.setImage_ = function(imageId) {
|
2014-02-17 22:28:20 +00:00
|
|
|
// Find the new current image.
|
|
|
|
var newImage = this.findImage_(function(image) {
|
|
|
|
return image.id == imageId;
|
|
|
|
});
|
2013-10-11 00:43:37 +00:00
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
if (newImage == this.currentImage_) {
|
|
|
|
return;
|
|
|
|
}
|
2013-10-11 00:43:37 +00:00
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
this.setHighlightedPath_(newImage);
|
|
|
|
this.currentImage_ = newImage;
|
|
|
|
this.currentTag_ = null;
|
|
|
|
this.update_(this.root_);
|
2013-10-10 21:13:42 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
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")
|
2014-02-17 22:28:20 +00:00
|
|
|
.data(nodes, function(d) { return d.id || (d.id = that.idCounter_++); });
|
2013-10-10 04:40:18 +00:00
|
|
|
|
|
|
|
// Enter any new nodes at the parent's previous position.
|
|
|
|
var nodeEnter = node.enter().append("svg:g")
|
2014-02-17 22:28:20 +00:00
|
|
|
.attr("class", "node")
|
|
|
|
.attr("transform", function(d) { return "translate(" + source.x0 + "," + source.y0 + ")"; });
|
2013-10-10 04:40:18 +00:00
|
|
|
|
|
|
|
nodeEnter.append("svg:circle")
|
2014-02-17 22:28:20 +00:00
|
|
|
.attr("r", 1e-6)
|
|
|
|
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; })
|
|
|
|
.on("click", function(d) { that.toggle_(d); that.update_(d); });
|
|
|
|
|
2013-10-10 06:09:17 +00:00
|
|
|
// 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")
|
2014-02-17 22:28:20 +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; })
|
2014-04-08 00:11:41 +00:00
|
|
|
.on("click", function(d) {
|
|
|
|
if (d.image) { that.changeImage_(d.image.id); }
|
|
|
|
if (d.collapsed) { that.expandCollapsed_(d); }
|
|
|
|
})
|
2014-02-17 22:28:20 +00:00
|
|
|
.on('mouseover', tip.show)
|
|
|
|
.on('mouseout', tip.hide);
|
|
|
|
|
|
|
|
nodeEnter.selectAll("tags")
|
|
|
|
.append("svg:text")
|
|
|
|
.text("bar");
|
2013-10-10 06:09:17 +00:00
|
|
|
|
|
|
|
// Create the foreign object to hold the tags (if any).
|
|
|
|
var fo = g.append("svg:foreignObject")
|
2014-02-17 22:28:20 +00:00
|
|
|
.attr("class", "fo")
|
|
|
|
.attr("x", 14)
|
|
|
|
.attr("y", 12)
|
|
|
|
.attr("width", 110)
|
|
|
|
.attr("height", DEPTH_HEIGHT - 20);
|
2013-10-10 06:09:17 +00:00
|
|
|
|
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) {
|
|
|
|
return "translate(" + [-130, 0 ] + ")";
|
|
|
|
});
|
|
|
|
|
2013-10-10 04:40:18 +00:00
|
|
|
// Transition nodes to their new position.
|
|
|
|
var nodeUpdate = node.transition()
|
2014-02-17 22:28:20 +00:00
|
|
|
.duration(duration)
|
|
|
|
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
|
2013-10-10 04:40:18 +00:00
|
|
|
|
2013-10-10 21:13:42 +00:00
|
|
|
// Update the node circle.
|
2013-10-10 04:40:18 +00:00
|
|
|
nodeUpdate.select("circle")
|
2014-02-17 22:28:20 +00:00
|
|
|
.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-10 04:40:18 +00:00
|
|
|
|
2013-10-11 00:43:37 +00:00
|
|
|
// Update the repo text.
|
|
|
|
nodeUpdate.select("text")
|
2014-02-17 22:28:20 +00:00
|
|
|
.attr("class", function(d) {
|
|
|
|
if (d.collapsed) {
|
|
|
|
return 'collapsed';
|
|
|
|
}
|
|
|
|
if (d.virtual) {
|
|
|
|
return 'virtual';
|
|
|
|
}
|
2014-04-08 00:11:41 +00:00
|
|
|
if (!currentImage) {
|
|
|
|
return '';
|
|
|
|
}
|
2014-02-17 22:28:20 +00:00
|
|
|
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")
|
2014-02-17 22:28:20 +00:00
|
|
|
.style("fill-opacity", 1);
|
2013-10-10 04:40:18 +00:00
|
|
|
|
2013-10-10 21:13:42 +00:00
|
|
|
// Update the tags.
|
|
|
|
node.select(".tags")
|
2014-02-17 22:28:20 +00:00
|
|
|
.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';
|
|
|
|
}
|
2014-04-07 23:53:25 +00:00
|
|
|
html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '" title="' + tag + '">' + tag + '</span>';
|
2014-02-17 22:28:20 +00:00
|
|
|
}
|
|
|
|
return html;
|
|
|
|
});
|
2013-10-10 21:13:42 +00:00
|
|
|
|
|
|
|
// Listen for click events on the labels.
|
|
|
|
node.selectAll(".tag")
|
2014-02-17 22:28:20 +00:00
|
|
|
.on("click", function(d, e) {
|
|
|
|
var tag = this.getAttribute('data-tag');
|
|
|
|
if (tag) {
|
|
|
|
that.changeTag_(tag);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.on("contextmenu", function(d, e) {
|
|
|
|
d3.event.preventDefault();
|
|
|
|
|
|
|
|
var tag = this.getAttribute('data-tag');
|
|
|
|
if (tag) {
|
|
|
|
$(that).trigger({
|
|
|
|
'type': 'showTagMenu',
|
|
|
|
'tag': tag,
|
|
|
|
'clientX': d3.event.clientX,
|
|
|
|
'clientY': d3.event.clientY
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
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) {
|
2014-02-17 22:28:20 +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()
|
2014-02-17 22:28:20 +00:00
|
|
|
.duration(duration)
|
|
|
|
.attr("transform", function(d) { return "translate(" + source.x + "," + source.y + ")"; })
|
|
|
|
.remove();
|
2013-10-10 04:40:18 +00:00
|
|
|
|
|
|
|
nodeExit.select("circle")
|
2014-02-17 22:28:20 +00:00
|
|
|
.attr("r", 1e-6);
|
2013-10-10 04:40:18 +00:00
|
|
|
|
2013-10-10 06:09:17 +00:00
|
|
|
nodeExit.select(".tags")
|
2014-02-17 22:28:20 +00:00
|
|
|
.style("display", "none");
|
2013-10-10 06:09:17 +00:00
|
|
|
|
|
|
|
nodeExit.select("g")
|
2014-02-17 22:28:20 +00:00
|
|
|
.style("fill-opacity", 1e-6);
|
2013-10-10 04:40:18 +00:00
|
|
|
|
|
|
|
// Update the links...
|
|
|
|
var link = vis.selectAll("path.link")
|
2014-02-17 22:28:20 +00:00
|
|
|
.data(tree.links(nodes), function(d) { return d.target.id; });
|
2013-10-10 04:40:18 +00:00
|
|
|
|
|
|
|
// Enter any new links at the parent's previous position.
|
|
|
|
link.enter().insert("svg:path", "g")
|
2014-02-17 22:28:20 +00:00
|
|
|
.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});
|
|
|
|
})
|
2013-10-10 04:40:18 +00:00
|
|
|
.transition()
|
2014-02-17 22:28:20 +00:00
|
|
|
.duration(duration)
|
|
|
|
.attr("d", diagonal);
|
2013-10-10 04:40:18 +00:00
|
|
|
|
|
|
|
// Transition links to their new position.
|
|
|
|
link.transition()
|
2014-02-17 22:28:20 +00:00
|
|
|
.duration(duration)
|
|
|
|
.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()
|
2014-02-17 22:28:20 +00:00
|
|
|
.duration(duration)
|
|
|
|
.attr("d", function(d) {
|
|
|
|
var o = {x: source.x, y: source.y};
|
|
|
|
return diagonal({source: o, target: o});
|
|
|
|
})
|
|
|
|
.remove();
|
2013-10-10 04:40:18 +00:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
2013-10-19 23:46:30 +00:00
|
|
|
};
|
|
|
|
|
2013-10-30 20:13:34 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Disposes of the tree.
|
|
|
|
*/
|
|
|
|
ImageHistoryTree.prototype.dispose = function() {
|
|
|
|
var container = this.container_ ;
|
|
|
|
$('#' + container).removeOverscroll();
|
|
|
|
document.getElementById(container).innerHTML = '';
|
|
|
|
};
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
2013-10-19 23:46:30 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Based off of http://bl.ocks.org/mbostock/1093025 by Mike Bostock (@mbostock)
|
|
|
|
*/
|
2014-02-17 22:28:20 +00:00
|
|
|
function FileTreeBase() {
|
|
|
|
/**
|
|
|
|
* Counter for creating unique IDs.
|
|
|
|
*/
|
|
|
|
this.idCounter_ = 0;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Map from file path to associated tree node.
|
|
|
|
*/
|
|
|
|
this.nodeMap_ = {};
|
2013-10-19 23:46:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Calculates the dimensions of the tree.
|
|
|
|
*/
|
2014-02-17 22:28:20 +00:00
|
|
|
FileTreeBase.prototype.calculateDimensions_ = function(container) {
|
2014-03-05 21:27:56 +00:00
|
|
|
var containerElm = document.getElementById(container);
|
|
|
|
var cw = containerElm ? containerElm.clientWidth : 1200;
|
2014-02-17 22:28:20 +00:00
|
|
|
var barHeight = 20;
|
|
|
|
var ch = (this.getNodesHeight() * barHeight) + 40;
|
|
|
|
|
|
|
|
var margin = { top: 40, right: 00, bottom: 20, left: 20 };
|
|
|
|
var m = [margin.top, margin.right, margin.bottom, margin.left];
|
2014-02-18 02:21:55 +00:00
|
|
|
var w = Math.max(0, cw - m[1] - m[3]);
|
|
|
|
var h = Math.max(0, ch - m[0] - m[2]);
|
2014-02-17 22:28:20 +00:00
|
|
|
|
2014-02-18 02:21:55 +00:00
|
|
|
var barWidth = Math.max(0, cw * 0.8 - m[1] - m[3]);
|
2014-02-17 22:28:20 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
'w': w,
|
|
|
|
'h': h,
|
|
|
|
'm': m,
|
|
|
|
'cw': cw,
|
|
|
|
'ch': ch,
|
|
|
|
'bw': barWidth,
|
|
|
|
'bh': barHeight
|
|
|
|
};
|
2013-10-19 23:46:30 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
2013-10-20 17:46:24 +00:00
|
|
|
/**
|
|
|
|
* Updates the dimensions of the tree.
|
|
|
|
*/
|
2014-02-17 22:28:20 +00:00
|
|
|
FileTreeBase.prototype.updateDimensions_ = function() {
|
|
|
|
var container = this.container_;
|
|
|
|
var dimensions = this.calculateDimensions_(container);
|
2013-10-20 17:46:24 +00:00
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
var w = dimensions.w;
|
|
|
|
var h = dimensions.h;
|
|
|
|
var m = dimensions.m;
|
2013-10-20 17:46:24 +00:00
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
// Update the tree.
|
|
|
|
var rootSvg = this.rootSvg_;
|
|
|
|
var tree = this.tree_;
|
|
|
|
var vis = this.vis_;
|
2013-10-20 17:46:24 +00:00
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
rootSvg
|
|
|
|
.attr("width", w + m[1] + m[3])
|
|
|
|
.attr("height", h + m[0] + m[2]);
|
2013-10-20 17:46:24 +00:00
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
tree.size([h, 100]);
|
|
|
|
vis.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
|
2013-10-20 17:46:24 +00:00
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
this.barWidth_ = dimensions.bw;
|
|
|
|
this.barHeight_ = dimensions.bh;
|
2013-10-20 17:46:24 +00:00
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
return dimensions;
|
2013-10-20 17:46:24 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Redraws the image change tree to fit the new size.
|
|
|
|
*/
|
2014-02-17 22:28:20 +00:00
|
|
|
FileTreeBase.prototype.notifyResized = function() {
|
2013-10-20 17:46:24 +00:00
|
|
|
this.updateDimensions_();
|
|
|
|
this.update_(this.root_);
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2013-10-30 17:55:44 +00:00
|
|
|
/**
|
|
|
|
* Disposes of the tree.
|
|
|
|
*/
|
2014-02-17 22:28:20 +00:00
|
|
|
FileTreeBase.prototype.dispose = function() {
|
2013-10-30 17:55:44 +00:00
|
|
|
var container = this.container_ ;
|
2013-10-30 20:13:34 +00:00
|
|
|
document.getElementById(container).innerHTML = '';
|
2013-10-30 17:55:44 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
2013-10-19 23:46:30 +00:00
|
|
|
/**
|
|
|
|
* Draws the tree.
|
|
|
|
*/
|
2014-02-17 22:28:20 +00:00
|
|
|
FileTreeBase.prototype.draw = function(container) {
|
2013-10-19 23:46:30 +00:00
|
|
|
this.container_ = container;
|
2014-02-17 22:28:20 +00:00
|
|
|
|
2013-10-19 23:46:30 +00:00
|
|
|
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")
|
2014-02-17 22:28:20 +00:00
|
|
|
.attr('class', 'file-tree-base')
|
2013-10-19 23:46:30 +00:00
|
|
|
.attr("width", w)
|
|
|
|
.attr("height", h);
|
|
|
|
|
|
|
|
var vis = rootSvg
|
2014-02-17 22:28:20 +00:00
|
|
|
.append("svg:g")
|
|
|
|
.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
|
2013-10-19 23:46:30 +00:00
|
|
|
|
|
|
|
this.rootSvg_ = rootSvg;
|
|
|
|
this.tree_ = tree;
|
|
|
|
this.diagonal_ = diagonal;
|
|
|
|
this.vis_ = vis;
|
|
|
|
|
|
|
|
this.populateAndDraw_();
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Populates the tree and then draws it.
|
|
|
|
*/
|
2014-02-17 22:28:20 +00:00
|
|
|
FileTreeBase.prototype.populateAndDraw_ = function() {
|
|
|
|
// Build all the nodes for the file tree.
|
|
|
|
this.buildAllNodes();
|
2013-10-19 23:46:30 +00:00
|
|
|
|
|
|
|
// 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.
|
|
|
|
*/
|
2014-02-17 22:28:20 +00:00
|
|
|
FileTreeBase.prototype.buildNodes_ = function(path) {
|
2013-10-19 23:46:30 +00:00
|
|
|
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('/');
|
2014-02-17 22:28:20 +00:00
|
|
|
var parent = this.buildNodes_(parentPath);
|
|
|
|
parent._children.push(this.nodeMap_[currentPath]);
|
2013-10-19 23:46:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return this.nodeMap_[path];
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Calculates the count of visible nodes.
|
|
|
|
*/
|
2014-02-17 22:28:20 +00:00
|
|
|
FileTreeBase.prototype.getVisibleCount_ = function(node) {
|
2013-10-19 23:46:30 +00:00
|
|
|
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.
|
|
|
|
*/
|
2014-02-17 22:28:20 +00:00
|
|
|
FileTreeBase.prototype.getContainerHeight_ = function() {
|
2013-10-19 23:46:30 +00:00
|
|
|
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.
|
|
|
|
*/
|
2014-02-17 22:28:20 +00:00
|
|
|
FileTreeBase.prototype.update_ = function(source) {
|
2013-10-19 23:46:30 +00:00
|
|
|
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) {
|
2014-02-17 22:28:20 +00:00
|
|
|
if (d.kind) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
return d._children ? "#E9E9E9" : "#c6dbef";
|
2013-10-19 23:46:30 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// 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")
|
2014-02-17 22:28:20 +00:00
|
|
|
.data(nodes, function(d) { return d.id || (d.id = that.idCounter_++); });
|
2013-10-19 23:46:30 +00:00
|
|
|
|
|
|
|
var nodeEnter = node.enter().append("svg:g")
|
2014-02-17 22:28:20 +00:00
|
|
|
.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);
|
2013-10-19 23:46:30 +00:00
|
|
|
|
|
|
|
// Enter any new nodes at the parent's previous position.
|
|
|
|
nodeEnter.append("svg:rect")
|
2014-02-17 22:28:20 +00:00
|
|
|
.attr("class", "main-rect")
|
|
|
|
.attr("y", -barHeight / 2)
|
|
|
|
.attr("height", barHeight)
|
|
|
|
.attr("width", barWidth)
|
|
|
|
.style("fill", color)
|
|
|
|
.on("click", function(d) {
|
|
|
|
if (d.kind) {
|
|
|
|
$(that).trigger({
|
|
|
|
'type': 'fileClicked',
|
|
|
|
'path': d.path
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
that.toggle_(d);
|
|
|
|
that.update_(source);
|
|
|
|
});
|
|
|
|
|
2013-10-19 23:46:30 +00:00
|
|
|
nodeEnter.append("svg:text")
|
2014-02-17 22:28:20 +00:00
|
|
|
.attr("dy", 3.5)
|
|
|
|
.attr("dx", 5.5 + 18)
|
|
|
|
.text(function(d) { return d.name; });
|
2013-10-19 23:46:30 +00:00
|
|
|
|
|
|
|
var body = nodeEnter.append('svg:foreignObject')
|
2014-02-17 22:28:20 +00:00
|
|
|
.attr("class", "fo")
|
|
|
|
.attr("width", 18)
|
|
|
|
.attr("height", barHeight)
|
|
|
|
.append('xhtml:body');
|
2013-10-19 23:46:30 +00:00
|
|
|
|
|
|
|
body.append('div')
|
2014-02-17 22:28:20 +00:00
|
|
|
.attr('class', 'node-icon');
|
2013-10-19 23:46:30 +00:00
|
|
|
|
|
|
|
// Transition nodes to their new position.
|
|
|
|
nodeEnter.transition()
|
2014-02-17 22:28:20 +00:00
|
|
|
.duration(duration)
|
|
|
|
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
|
|
|
|
.style("opacity", 1);
|
|
|
|
|
2013-10-19 23:46:30 +00:00
|
|
|
node.transition()
|
2014-02-17 22:28:20 +00:00
|
|
|
.duration(duration)
|
|
|
|
// TODO: reenable for full animation
|
|
|
|
//.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
|
|
|
|
.style("opacity", 1)
|
2013-10-19 23:46:30 +00:00
|
|
|
.select("rect")
|
2014-02-17 22:28:20 +00:00
|
|
|
.style("fill", color);
|
2013-10-19 23:46:30 +00:00
|
|
|
|
|
|
|
// TODO: remove if full animation.
|
|
|
|
node.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
|
2013-10-20 17:46:24 +00:00
|
|
|
|
|
|
|
node.select('.main-rect')
|
2014-02-17 22:28:20 +00:00
|
|
|
.attr("y", -barHeight / 2)
|
|
|
|
.attr("height", barHeight)
|
|
|
|
.attr("width", barWidth);
|
2013-10-20 17:46:24 +00:00
|
|
|
|
|
|
|
node.select('.fo')
|
2014-02-17 22:28:20 +00:00
|
|
|
.attr("x", function(d) { return d.kind ? barWidth - 18 : 0; })
|
|
|
|
.attr("y", -10)
|
2013-10-19 23:46:30 +00:00
|
|
|
|
|
|
|
node.select('.node-icon')
|
2014-02-17 22:28:20 +00:00
|
|
|
.html(function(d) {
|
|
|
|
if (!d.kind) {
|
|
|
|
var folder = d._children ? 'fa fa-folder' : 'fa fa-folder-open';
|
|
|
|
return '<i class="' + folder + '"></i>';
|
|
|
|
}
|
|
|
|
|
|
|
|
return that.determineIcon(d);
|
|
|
|
});
|
2013-10-19 23:46:30 +00:00
|
|
|
|
|
|
|
// Transition exiting nodes to the parent's new position.
|
|
|
|
node.exit().transition()
|
2014-02-17 22:28:20 +00:00
|
|
|
.duration(duration)
|
|
|
|
// TODO: reenable for full animation
|
|
|
|
// .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; })
|
|
|
|
.style("opacity", 1e-6)
|
|
|
|
.remove();
|
2013-10-19 23:46:30 +00:00
|
|
|
|
|
|
|
// Update the links...
|
|
|
|
var link = vis.selectAll("path.link")
|
2014-02-17 22:28:20 +00:00
|
|
|
.data(tree.links(nodes), function(d) { return d.target.id; });
|
2013-10-19 23:46:30 +00:00
|
|
|
|
|
|
|
// Enter any new links at the parent's previous position.
|
|
|
|
link.enter().insert("svg:path", "g")
|
2014-02-17 22:28:20 +00:00
|
|
|
.attr("class", "link")
|
|
|
|
.attr("d", function(d) {
|
|
|
|
var o = {x: source.x0, y: source.y0};
|
|
|
|
return diagonal({source: o, target: o});
|
|
|
|
})
|
2013-10-19 23:46:30 +00:00
|
|
|
.transition()
|
2014-02-17 22:28:20 +00:00
|
|
|
.duration(duration)
|
|
|
|
.attr("d", diagonal);
|
2013-10-19 23:46:30 +00:00
|
|
|
|
|
|
|
// Transition links to their new position.
|
|
|
|
link.transition()
|
2014-02-17 22:28:20 +00:00
|
|
|
.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});
|
|
|
|
});
|
2013-10-19 23:46:30 +00:00
|
|
|
|
|
|
|
// Transition exiting nodes to the parent's new position.
|
|
|
|
link.exit().transition()
|
2014-02-17 22:28:20 +00:00
|
|
|
.duration(duration)
|
|
|
|
.attr("d", function(d) {
|
|
|
|
var o = {x: source.x, y: source.y};
|
|
|
|
return diagonal({source: o, target: o});
|
|
|
|
})
|
|
|
|
.remove();
|
2013-10-19 23:46:30 +00:00
|
|
|
|
|
|
|
// Stash the old positions for transition.
|
|
|
|
nodes.forEach(function(d) {
|
|
|
|
d.x0 = d.x;
|
|
|
|
d.y0 = d.y;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Toggles children of a node.
|
|
|
|
*/
|
2014-02-17 22:28:20 +00:00
|
|
|
FileTreeBase.prototype.toggle_ = function(d) {
|
2013-10-19 23:46:30 +00:00
|
|
|
if (d.children) {
|
|
|
|
d._children = d.children;
|
|
|
|
d.children = null;
|
|
|
|
} else {
|
|
|
|
d.children = d._children;
|
|
|
|
d._children = null;
|
|
|
|
}
|
2013-11-06 22:30:20 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
2014-02-17 22:28:20 +00:00
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
function ImageFileChangeTree(image, changes) {
|
|
|
|
FileTreeBase.apply(this);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The parent image.
|
|
|
|
*/
|
|
|
|
this.image_ = image;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The changes being drawn.
|
|
|
|
*/
|
|
|
|
this.changes_ = changes;
|
|
|
|
}
|
|
|
|
|
|
|
|
$.extend(ImageFileChangeTree.prototype, FileTreeBase.prototype);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Builds all the file nodes from the changes.
|
|
|
|
*/
|
|
|
|
ImageFileChangeTree.prototype.buildAllNodes = function() {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines the icon for a node.
|
|
|
|
*/
|
|
|
|
ImageFileChangeTree.prototype.determineIcon = function(d) {
|
|
|
|
var icon = {
|
|
|
|
'added': 'plus-square',
|
|
|
|
'removed': 'minus-square',
|
|
|
|
'changed': 'pencil-square'
|
|
|
|
};
|
|
|
|
|
|
|
|
return '<i class="change-icon fa fa-' + icon[d.kind] + '"></i>';
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the max height of the tree.
|
|
|
|
*/
|
|
|
|
ImageFileChangeTree.prototype.getNodesHeight = function() {
|
|
|
|
return this.changes_.length;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
function FileTree(files) {
|
|
|
|
FileTreeBase.apply(this);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The set of file paths found in the tree.
|
|
|
|
*/
|
|
|
|
this.files_ = files;
|
|
|
|
}
|
|
|
|
|
|
|
|
$.extend(FileTree.prototype, FileTreeBase.prototype);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Builds all the file nodes from the paths.
|
|
|
|
*/
|
|
|
|
FileTree.prototype.buildAllNodes = function() {
|
|
|
|
for (var i = 0; i < this.files_.length; ++i) {
|
|
|
|
var filepath = this.files_[i];
|
|
|
|
if (filepath[filepath.length - 1] == '/') { continue; }
|
|
|
|
var node = this.buildNodes_(filepath);
|
|
|
|
node.kind = 'file';
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines the icon for a node.
|
|
|
|
*/
|
|
|
|
FileTree.prototype.determineIcon = function(d) {
|
|
|
|
return '';
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the max height of the tree.
|
|
|
|
*/
|
|
|
|
FileTree.prototype.getNodesHeight = function() {
|
|
|
|
return this.files_.length;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2013-11-06 22:30:20 +00:00
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Based off of http://bl.ocks.org/mbostock/1346410
|
|
|
|
*/
|
2014-04-10 04:26:55 +00:00
|
|
|
function UsageChart() {
|
2013-11-06 22:30:20 +00:00
|
|
|
this.total_ = null;
|
|
|
|
this.count_ = null;
|
|
|
|
this.drawn_ = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Updates the chart with the given count and total of number of repositories.
|
|
|
|
*/
|
2014-04-10 04:26:55 +00:00
|
|
|
UsageChart.prototype.update = function(count, total) {
|
2013-11-06 22:30:20 +00:00
|
|
|
if (!this.g_) { return; }
|
|
|
|
this.total_ = total;
|
|
|
|
this.count_ = count;
|
|
|
|
this.drawInternal_();
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Conducts the actual draw or update (if applicable).
|
|
|
|
*/
|
2014-04-10 04:26:55 +00:00
|
|
|
UsageChart.prototype.drawInternal_ = function() {
|
2013-11-06 22:30:20 +00:00
|
|
|
// 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_;
|
|
|
|
|
2013-11-07 20:19:52 +00:00
|
|
|
var data = [Math.max(count, 1), Math.max(0, total - count)];
|
2013-11-06 22:30:20 +00:00
|
|
|
|
|
|
|
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")
|
2014-02-17 22:28:20 +00:00
|
|
|
.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
|
2013-11-06 22:30:20 +00:00
|
|
|
|
|
|
|
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_);
|
|
|
|
}
|
2014-02-17 22:28:20 +00:00
|
|
|
|
2013-11-06 22:30:20 +00:00
|
|
|
this.drawn_ = true;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Draws the chart in the given container.
|
|
|
|
*/
|
2014-04-10 04:26:55 +00:00
|
|
|
UsageChart.prototype.draw = function(container) {
|
2013-11-08 03:08:23 +00:00
|
|
|
var cw = 200;
|
|
|
|
var ch = 200;
|
2013-11-06 22:30:20 +00:00
|
|
|
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_();
|
2013-11-27 07:29:31 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A chart which displays the last seven days of actions in the account.
|
|
|
|
*/
|
2013-11-27 21:56:07 +00:00
|
|
|
function LogUsageChart(titleMap) {
|
2013-11-27 07:29:31 +00:00
|
|
|
this.titleMap_ = titleMap;
|
|
|
|
this.colorScale_ = d3.scale.category20();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Builds the D3-representation of the data.
|
|
|
|
*/
|
2013-11-27 21:56:07 +00:00
|
|
|
LogUsageChart.prototype.buildData_ = function(logs) {
|
2014-03-17 17:10:12 +00:00
|
|
|
var parseDate = d3.time.format("%a, %d %b %Y %H:%M:%S %Z").parse
|
2013-11-27 07:29:31 +00:00
|
|
|
|
|
|
|
// Build entries for each kind of event that occurred, on each day. We have one
|
|
|
|
// entry per {kind, day} pair.
|
|
|
|
var map = {};
|
|
|
|
var entries = [];
|
2013-11-27 21:56:07 +00:00
|
|
|
for (var i = 0; i < logs.length; ++i) {
|
|
|
|
var log = logs[i];
|
2013-11-27 07:29:31 +00:00
|
|
|
var title = this.titleMap_[log.kind] || log.kind;
|
|
|
|
var datetime = parseDate(log.datetime);
|
2013-12-02 19:55:04 +00:00
|
|
|
var dateDay = datetime.getDate();
|
|
|
|
if (dateDay < 10) {
|
|
|
|
dateDay = '0' + dateDay;
|
|
|
|
}
|
|
|
|
|
|
|
|
var formatted = (datetime.getMonth() + 1) + '/' + dateDay;
|
2013-11-27 07:29:31 +00:00
|
|
|
var adjusted = new Date(datetime.getFullYear(), datetime.getMonth(), datetime.getDate());
|
|
|
|
var key = title + '_' + formatted;
|
|
|
|
var found = map[key];
|
|
|
|
if (!found) {
|
|
|
|
found = {
|
|
|
|
'kind': log.kind,
|
|
|
|
'title': title,
|
|
|
|
'adjusted': adjusted,
|
|
|
|
'formatted': datetime.getDate(),
|
|
|
|
'count': 0
|
|
|
|
};
|
|
|
|
|
|
|
|
map[key] = found;
|
|
|
|
entries.push(found);
|
|
|
|
}
|
|
|
|
found['count']++;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.entries_ = map;
|
|
|
|
|
|
|
|
// Build the data itself. We create a single entry for each possible kind of data, and then add (x, y) pairs
|
|
|
|
// for the number of times that kind of event occurred on a particular day.
|
|
|
|
var dataArray = [];
|
|
|
|
var dataMap = {};
|
|
|
|
var dateMap = {};
|
|
|
|
|
|
|
|
for (var i = 0; i < entries.length; ++i) {
|
|
|
|
var entry = entries[i];
|
|
|
|
var key = entry.title;
|
|
|
|
var found = dataMap[key];
|
|
|
|
if (!found) {
|
|
|
|
found = {'key': key, 'values': [], 'kind': entry.kind};
|
|
|
|
dataMap[key] = found;
|
|
|
|
dataArray.push(found);
|
|
|
|
}
|
2014-02-17 22:28:20 +00:00
|
|
|
|
2013-11-27 07:29:31 +00:00
|
|
|
found.values.push({
|
|
|
|
'x': entry.adjusted,
|
|
|
|
'y': entry.count
|
|
|
|
});
|
|
|
|
|
|
|
|
dateMap[entry.adjusted.toString()] = entry.adjusted;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Note: nvd3 has a bug that causes d3 to fail if there is not an entry for every single
|
|
|
|
// kind on each day that has data. Therefore, we pad those days with 0-length entries for each
|
|
|
|
// kind.
|
|
|
|
for (var i = 0; i < dataArray.length; ++i) {
|
|
|
|
var datum = dataArray[i];
|
|
|
|
for (var sDate in dateMap) {
|
|
|
|
if (!dateMap.hasOwnProperty(sDate)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
var cDate = dateMap[sDate];
|
|
|
|
var found = false;
|
|
|
|
for (var j = 0; j < datum.values.length; ++j) {
|
|
|
|
if (datum.values[j]['x'].getDate() == cDate.getDate()) {
|
|
|
|
found = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!found) {
|
|
|
|
datum.values.push({
|
|
|
|
'x': cDate,
|
|
|
|
'y': 0
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
datum.values.sort(function(a, b) {
|
|
|
|
return a['x'].getDate() - b['x'].getDate();
|
|
|
|
});
|
|
|
|
}
|
2014-02-17 22:28:20 +00:00
|
|
|
|
2013-11-27 07:29:31 +00:00
|
|
|
return this.data_ = dataArray;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Renders the tooltip when hovering over an element in the chart.
|
|
|
|
*/
|
|
|
|
LogUsageChart.prototype.renderTooltip_ = function(d, e) {
|
2014-01-21 22:07:22 +00:00
|
|
|
if (e[0] == '0') {
|
|
|
|
e = e.substr(1);
|
|
|
|
}
|
|
|
|
|
2013-12-02 19:55:04 +00:00
|
|
|
var key = d + '_' + e;
|
|
|
|
var entry = this.entries_[key];
|
2013-11-27 07:29:31 +00:00
|
|
|
if (!entry) {
|
|
|
|
entry = {'count': 0};
|
|
|
|
}
|
|
|
|
|
|
|
|
var s = entry.count == 1 ? '' : 's';
|
|
|
|
return d + ' - ' + entry.count + ' time' + s + ' on ' + e;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the color used in the chart for log entries of the given
|
|
|
|
* kind.
|
|
|
|
*/
|
|
|
|
LogUsageChart.prototype.getColor = function(kind) {
|
|
|
|
var colors = this.colorScale_.range();
|
|
|
|
var index = 0;
|
|
|
|
for (var i = 0; i < this.data_.length; ++i) {
|
|
|
|
var datum = this.data_[i];
|
|
|
|
var key = this.titleMap_[kind] || kind;
|
|
|
|
if (datum.key == key) {
|
|
|
|
index = i;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return colors[index];
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2013-12-02 22:52:20 +00:00
|
|
|
/**
|
|
|
|
* Handler for when an element in the chart has been clicked.
|
|
|
|
*/
|
|
|
|
LogUsageChart.prototype.handleElementClicked_ = function(e) {
|
|
|
|
var key = e.series.key;
|
|
|
|
var kind = e.series.kind;
|
|
|
|
var disabled = [];
|
|
|
|
|
2013-12-02 23:20:42 +00:00
|
|
|
var enabledCount = 0;
|
2013-12-02 23:21:15 +00:00
|
|
|
var d = this.chart_.multibar.disabled();
|
2013-12-02 22:52:20 +00:00
|
|
|
for (var i = 0; i < this.data_.length; ++i) {
|
2013-12-02 23:21:15 +00:00
|
|
|
enabledCount += (d[i] ? 0 : 1);
|
2013-12-02 23:20:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for (var i = 0; i < this.data_.length; ++i) {
|
|
|
|
disabled.push(enabledCount == 1 ? false : this.data_[i].key != key);
|
2013-12-02 22:52:20 +00:00
|
|
|
}
|
|
|
|
|
2013-12-02 22:58:45 +00:00
|
|
|
var allowed = {};
|
|
|
|
allowed[kind] = true;
|
|
|
|
|
2013-12-02 22:52:20 +00:00
|
|
|
this.chart_.dispatch.changeState({ 'disabled': disabled });
|
2013-12-02 22:58:45 +00:00
|
|
|
$(this).trigger({
|
|
|
|
'type': 'filteringChanged',
|
2013-12-02 23:20:42 +00:00
|
|
|
'allowed': enabledCount == 1 ? null : allowed
|
2013-12-02 22:58:45 +00:00
|
|
|
});
|
2013-12-02 22:52:20 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handler for when the state of the chart has changed.
|
|
|
|
*/
|
2013-11-27 07:29:31 +00:00
|
|
|
LogUsageChart.prototype.handleStateChange_ = function(e) {
|
|
|
|
var allowed = {};
|
|
|
|
var disabled = e.disabled;
|
|
|
|
for (var i = 0; i < this.data_.length; ++i) {
|
|
|
|
if (!disabled[i]) {
|
|
|
|
allowed[this.data_[i].kind] = true;
|
|
|
|
}
|
|
|
|
}
|
2014-02-17 22:28:20 +00:00
|
|
|
|
2013-11-27 07:29:31 +00:00
|
|
|
$(this).trigger({
|
|
|
|
'type': 'filteringChanged',
|
|
|
|
'allowed': allowed
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Draws the chart in the given container element.
|
|
|
|
*/
|
2013-12-09 22:28:23 +00:00
|
|
|
LogUsageChart.prototype.draw = function(container, logData, startDate, endDate) {
|
2013-11-27 21:56:07 +00:00
|
|
|
// Reset the container's contents.
|
|
|
|
document.getElementById(container).innerHTML = '<svg></svg>';
|
|
|
|
|
2013-11-27 07:29:31 +00:00
|
|
|
// Returns a date offset from the given date by "days" Days.
|
|
|
|
var offsetDate = function(d, days) {
|
|
|
|
var copy = new Date(d.getTime());
|
|
|
|
copy.setDate(copy.getDate() + days);
|
|
|
|
return copy;
|
|
|
|
};
|
|
|
|
|
|
|
|
var that = this;
|
2013-11-27 21:56:07 +00:00
|
|
|
var data = this.buildData_(logData);
|
2013-11-27 07:29:31 +00:00
|
|
|
nv.addGraph(function() {
|
|
|
|
// Build the chart itself.
|
|
|
|
var chart = nv.models.multiBarChart()
|
|
|
|
.margin({top: 30, right: 30, bottom: 50, left: 30})
|
|
|
|
.stacked(false)
|
|
|
|
.staggerLabels(false)
|
|
|
|
.tooltip(function(d, e) {
|
|
|
|
return that.renderTooltip_(d, e);
|
|
|
|
})
|
|
|
|
.color(that.colorScale_.range())
|
|
|
|
.groupSpacing(0.1);
|
|
|
|
|
|
|
|
chart.multibar.delay(0);
|
|
|
|
|
2013-12-09 22:28:23 +00:00
|
|
|
// Create the x-axis domain to encompass the full date range.
|
2013-12-04 01:08:02 +00:00
|
|
|
var domain = [];
|
2013-12-09 22:28:23 +00:00
|
|
|
var datetime = startDate;
|
|
|
|
while (datetime <= endDate) {
|
|
|
|
domain.push(datetime);
|
|
|
|
datetime = offsetDate(datetime, 1);
|
2013-11-27 07:29:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
chart.xDomain(domain);
|
|
|
|
|
|
|
|
// Finish setting up the chart.
|
|
|
|
chart.xAxis
|
|
|
|
.tickFormat(d3.time.format("%m/%d"));
|
|
|
|
|
|
|
|
chart.yAxis
|
|
|
|
.tickFormat(d3.format(',f'));
|
|
|
|
|
|
|
|
d3.select('#bar-chart svg')
|
|
|
|
.datum(data)
|
|
|
|
.transition()
|
2014-02-17 22:28:20 +00:00
|
|
|
.duration(500)
|
|
|
|
.call(chart);
|
2013-11-27 07:29:31 +00:00
|
|
|
|
2014-04-07 23:53:25 +00:00
|
|
|
nv.utils.windoweResize(chart.update);
|
2013-11-27 07:29:31 +00:00
|
|
|
|
2013-12-02 22:52:20 +00:00
|
|
|
chart.multibar.dispatch.on('elementClick', function(e) { that.handleElementClicked_(e); });
|
2013-11-27 07:29:31 +00:00
|
|
|
chart.dispatch.on('stateChange', function(e) { that.handleStateChange_(e); });
|
2013-12-02 22:52:20 +00:00
|
|
|
return that.chart_ = chart;
|
2013-11-27 07:29:31 +00:00
|
|
|
});
|
2013-10-10 04:40:18 +00:00
|
|
|
};
|