Make the horrific tree look half decent. Also adds a nice scrolling feature to the tree
This commit is contained in:
parent
e4a0f70bc7
commit
04d4024d8c
5 changed files with 173 additions and 54 deletions
|
@ -717,6 +717,10 @@ p.editable:hover i {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#image-history-container {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
#image-history-container .node circle {
|
#image-history-container .node circle {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
fill: #fff;
|
fill: #fff;
|
||||||
|
@ -739,6 +743,10 @@ p.editable:hover i {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#image-history-container .node text.collapsed {
|
||||||
|
fill: gray;
|
||||||
|
}
|
||||||
|
|
||||||
#image-history-container .node text {
|
#image-history-container .node text {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -873,6 +881,7 @@ p.editable:hover i {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
|
z-index: 9999999;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Creates a small triangle extender for the tooltip */
|
/* Creates a small triangle extender for the tooltip */
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
var DEPTH_HEIGHT = 100;
|
var DEPTH_HEIGHT = 100;
|
||||||
|
var DEPTH_WIDTH = 132;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Based off of http://mbostock.github.io/d3/talk/20111018/tree.html by Mike Bostock (@mbostock)
|
* Based off of http://mbostock.github.io/d3/talk/20111018/tree.html by Mike Bostock (@mbostock)
|
||||||
|
@ -51,19 +52,32 @@ function ImageHistoryTree(namespace, name, images, current, formatComment, forma
|
||||||
*/
|
*/
|
||||||
ImageHistoryTree.prototype.draw = function(container) {
|
ImageHistoryTree.prototype.draw = function(container) {
|
||||||
// Build the root of the tree.
|
// Build the root of the tree.
|
||||||
this.maxHeight_ = this.buildRoot_();
|
var result = this.buildRoot_();
|
||||||
|
this.maxWidth_ = result['maxWidth'];
|
||||||
|
this.maxHeight_ = result['maxHeight'];
|
||||||
|
|
||||||
// Determine the size of the SVG container.
|
// Determine the size of the SVG container.
|
||||||
var width = document.getElementById(container).clientWidth;
|
var width = Math.max(document.getElementById(container).clientWidth, this.maxWidth_ * DEPTH_WIDTH);
|
||||||
var height = Math.max(width * 0.625, this.maxHeight_ * (DEPTH_HEIGHT + 10));
|
var height = this.maxHeight_ * (DEPTH_HEIGHT + 10);
|
||||||
|
|
||||||
var margin = { top: 40, right: 20, bottom: 20, left: 20 };
|
// Set the height of the container so that it never goes offscreen.
|
||||||
|
var viewportHeight = $(window).height();
|
||||||
|
var boundingBox = document.getElementById(container).getBoundingClientRect();
|
||||||
|
document.getElementById(container).style.maxHeight = (viewportHeight - boundingBox.top) + 'px';
|
||||||
|
|
||||||
|
var leftMargin = 20;
|
||||||
|
if (width > document.getElementById(container).clientWidth) {
|
||||||
|
leftMargin = 120;
|
||||||
|
}
|
||||||
|
|
||||||
|
var margin = { top: 40, right: 20, bottom: 20, left: leftMargin };
|
||||||
var m = [margin.top, margin.right, margin.bottom, margin.left];
|
var m = [margin.top, margin.right, margin.bottom, margin.left];
|
||||||
var w = width - m[1] - m[3];
|
var w = width - m[1] - m[3];
|
||||||
var h = height - m[0] - m[2];
|
var h = height - m[0] - m[2];
|
||||||
|
|
||||||
// Create the tree and all its components.
|
// Create the tree and all its components.
|
||||||
var tree = d3.layout.tree()
|
var tree = d3.layout.tree()
|
||||||
|
.separation(function() { return 2; })
|
||||||
.size([w, h]);
|
.size([w, h]);
|
||||||
|
|
||||||
var diagonal = d3.svg.diagonal()
|
var diagonal = d3.svg.diagonal()
|
||||||
|
@ -83,6 +97,14 @@ ImageHistoryTree.prototype.draw = function(container) {
|
||||||
.direction('e')
|
.direction('e')
|
||||||
.html(function(d) {
|
.html(function(d) {
|
||||||
var html = '';
|
var html = '';
|
||||||
|
if (d.collapsed) {
|
||||||
|
for (var i = 0; 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.comment) {
|
if (d.image.comment) {
|
||||||
html += '<span class="comment">' + formatComment(d.image.comment) + '</span>';
|
html += '<span class="comment">' + formatComment(d.image.comment) + '</span>';
|
||||||
}
|
}
|
||||||
|
@ -106,7 +128,11 @@ ImageHistoryTree.prototype.draw = function(container) {
|
||||||
this.tree_ = tree;
|
this.tree_ = tree;
|
||||||
|
|
||||||
// Populate the tree.
|
// Populate the tree.
|
||||||
this.populate_();
|
this.root_.x0 = this.fullWidth_ / 2;
|
||||||
|
this.root_.y0 = 0;
|
||||||
|
this.setTag_(this.currentTag_);
|
||||||
|
|
||||||
|
$('#' + container).overscroll();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -167,18 +193,6 @@ ImageHistoryTree.prototype.changeImage_ = function(imageId) {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Marks the image node with whether it is the current image and/or highlighted.
|
|
||||||
*/
|
|
||||||
ImageHistoryTree.prototype.markWithState_ = function(image, imageNode) {
|
|
||||||
var currentAncestors = this.getAncestors_(this.currentImage_);
|
|
||||||
var isCurrent = image.id == this.currentImage_.id;
|
|
||||||
var isHighlighted = currentAncestors.indexOf(image.dbid.toString()) >= 0;
|
|
||||||
imageNode.highlighted = isHighlighted || isCurrent;
|
|
||||||
imageNode.current = isCurrent;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the root node for the tree.
|
* Builds the root node for the tree.
|
||||||
*/
|
*/
|
||||||
|
@ -200,14 +214,12 @@ ImageHistoryTree.prototype.buildRoot_ = function() {
|
||||||
"image": image,
|
"image": image,
|
||||||
"tags": image.tags
|
"tags": image.tags
|
||||||
};
|
};
|
||||||
this.markWithState_(image, imageNode);
|
|
||||||
imageByDBID[image.dbid] = imageNode;
|
imageByDBID[image.dbid] = imageNode;
|
||||||
}
|
}
|
||||||
this.imageByDBID_ = imageByDBID;
|
this.imageByDBID_ = imageByDBID;
|
||||||
|
|
||||||
// For each node, attach it to its immediate parent. If there is no immediate parent,
|
// For each node, attach it to its immediate parent. If there is no immediate parent,
|
||||||
// then the node is the root.
|
// then the node is the root.
|
||||||
var maxAncestorCount = 0;
|
|
||||||
for (var i = 0; i < this.images_.length; ++i) {
|
for (var i = 0; i < this.images_.length; ++i) {
|
||||||
var image = this.images_[i];
|
var image = this.images_[i];
|
||||||
var imageNode = imageByDBID[image.dbid];
|
var imageNode = imageByDBID[image.dbid];
|
||||||
|
@ -215,22 +227,108 @@ ImageHistoryTree.prototype.buildRoot_ = function() {
|
||||||
var immediateParent = ancestors[ancestors.length - 1] * 1;
|
var immediateParent = ancestors[ancestors.length - 1] * 1;
|
||||||
var parent = imageByDBID[immediateParent];
|
var parent = imageByDBID[immediateParent];
|
||||||
if (parent) {
|
if (parent) {
|
||||||
// If the image node is highlighted, ensure it is at the front
|
// Add a reference to the parent. This makes walking the tree later easier.
|
||||||
// of the child list.
|
imageNode.parent = parent;
|
||||||
if (imageNode.highlighted) {
|
|
||||||
parent.children.splice(0, 0, imageNode);
|
|
||||||
} else {
|
|
||||||
parent.children.push(imageNode);
|
parent.children.push(imageNode);
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
formatted = imageNode;
|
formatted = imageNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
maxAncestorCount = Math.max(maxAncestorCount, ancestors.length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
this.root_ = formatted;
|
||||||
return maxAncestorCount + 1;
|
|
||||||
|
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;
|
||||||
|
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 + ' 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -250,25 +348,38 @@ ImageHistoryTree.prototype.findImage_ = function(checker) {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
* Sets the current tag displayed in the tree.
|
||||||
*/
|
*/
|
||||||
ImageHistoryTree.prototype.setTag_ = function(tagName) {
|
ImageHistoryTree.prototype.setTag_ = function(tagName) {
|
||||||
this.currentTag_ = tagName;
|
this.currentTag_ = tagName;
|
||||||
|
|
||||||
|
// Update the state of each existing node to no longer be highlighted.
|
||||||
|
var imageByDBID = this.imageByDBID_;
|
||||||
|
var currentNode = imageByDBID[this.currentImage_.dbid];
|
||||||
|
this.markPath_(currentNode, false);
|
||||||
|
|
||||||
// Find the new current image.
|
// Find the new current image.
|
||||||
this.currentImage_ = this.findImage_(function(image) {
|
this.currentImage_ = this.findImage_(function(image) {
|
||||||
return image.tags.indexOf(tagName) >= 0;
|
return image.tags.indexOf(tagName) >= 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update the state of each node.
|
// Update the state of the new node path.
|
||||||
var imageByDBID = this.imageByDBID_;
|
currentNode = imageByDBID[this.currentImage_.dbid];
|
||||||
var currentAncestors = this.getAncestors_(this.currentImage_);
|
this.markPath_(currentNode, true);
|
||||||
for (var i = 0; i < this.images_.length; ++i) {
|
|
||||||
var image = this.images_[i];
|
|
||||||
var imageNode = this.imageByDBID_[image.dbid];
|
|
||||||
this.markWithState_(image, imageNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that the children are in the correct order.
|
// Ensure that the children are in the correct order.
|
||||||
for (var i = 0; i < this.images_.length; ++i) {
|
for (var i = 0; i < this.images_.length; ++i) {
|
||||||
|
@ -285,12 +396,15 @@ ImageHistoryTree.prototype.setTag_ = function(tagName) {
|
||||||
|
|
||||||
if (arr[0] != imageNode) {
|
if (arr[0] != imageNode) {
|
||||||
var index = arr.indexOf(imageNode);
|
var index = arr.indexOf(imageNode);
|
||||||
|
if (index > 0) {
|
||||||
arr.splice(index, 1);
|
arr.splice(index, 1);
|
||||||
arr.splice(0, 0, imageNode);
|
arr.splice(0, 0, imageNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the tree.
|
||||||
this.update_(this.root_);
|
this.update_(this.root_);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -313,19 +427,6 @@ ImageHistoryTree.prototype.setImage_ = function(imageId) {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Populates the tree.
|
|
||||||
*/
|
|
||||||
ImageHistoryTree.prototype.populate_ = function() {
|
|
||||||
// Set the position of the initial node.
|
|
||||||
this.root_.x0 = this.fullWidth_ / 2;
|
|
||||||
this.root_.y0 = 0;
|
|
||||||
|
|
||||||
// Initialize the display to show the current path of nodes.
|
|
||||||
this.update_(this.root_);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the tree in response to a click on a node.
|
* Updates the tree in response to a click on a node.
|
||||||
*/
|
*/
|
||||||
|
@ -420,6 +521,9 @@ ImageHistoryTree.prototype.update_ = function(source) {
|
||||||
// Update the repo text.
|
// Update the repo text.
|
||||||
nodeUpdate.select("text")
|
nodeUpdate.select("text")
|
||||||
.attr("class", function(d) {
|
.attr("class", function(d) {
|
||||||
|
if (d.collapsed) {
|
||||||
|
return 'collapsed';
|
||||||
|
}
|
||||||
return d.image.id == that.currentImage_.id ? 'current' : '';
|
return d.image.id == that.currentImage_.id ? 'current' : '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -430,6 +534,10 @@ ImageHistoryTree.prototype.update_ = function(source) {
|
||||||
// Update the tags.
|
// Update the tags.
|
||||||
node.select(".tags")
|
node.select(".tags")
|
||||||
.html(function(d) {
|
.html(function(d) {
|
||||||
|
if (!d.tags) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
var html = '';
|
var html = '';
|
||||||
for (var i = 0; i < d.tags.length; ++i) {
|
for (var i = 0; i < d.tags.length; ++i) {
|
||||||
var tag = d.tags[i];
|
var tag = d.tags[i];
|
||||||
|
|
1
static/lib/jquery.overscroll.min.js
vendored
Normal file
1
static/lib/jquery.overscroll.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -100,7 +100,7 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
<span class="right-title">Images</span>
|
<span class="right-title">Selected Image</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
|
|
||||||
<script src="static/lib/angulartics.js"></script>
|
<script src="static/lib/angulartics.js"></script>
|
||||||
<script src="static/lib/angulartics-mixpanel.js"></script>
|
<script src="static/lib/angulartics-mixpanel.js"></script>
|
||||||
|
<script src="static/lib/jquery.overscroll.min.js"></script>
|
||||||
|
|
||||||
<script src="static/lib/angular-moment.min.js"></script>
|
<script src="static/lib/angular-moment.min.js"></script>
|
||||||
<script src="static/lib/pagedown/Markdown.Converter.js"></script>
|
<script src="static/lib/pagedown/Markdown.Converter.js"></script>
|
||||||
|
|
Reference in a new issue