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

283 lines
No EOL
8.1 KiB
JavaScript

/**
* Based off of http://mbostock.github.io/d3/talk/20111018/tree.html by Mike Bostock (@mbostock) and modifications
* from http://stackoverflow.com/questions/18108960/d3-tree-layout-custom-vertical-layout-when-children-exceed-more-than-a-certain
*/
function ImageHistoryTree(images, current, formatComment, formatTime) {
/**
* The images to display.
*/
this.images_ = images;
/**
* The current image.
*/
this.currentImage_ = current;
/**
* Counter for creating unique IDs.
*/
this.idCounter_ = 0;
/**
* 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;
}
/**
* Draws the tree.
*/
ImageHistoryTree.prototype.draw = function(container) {
var width = document.getElementById(container).clientWidth;
var height = width * 0.625;
var margin = { top: 40, right: 120, bottom: 20, left: 120 };
var m = [margin.top, margin.right, margin.bottom, margin.left];
var w = width - m[1] - m[3];
var h = height - m[0] - m[2];
var tree = d3.layout.tree()
.size([h, w]);
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.x, d.y]; });
var vis = d3.select("#" + container).append("svg:svg")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
var formatComment = this.formatComment_;
var formatTime = this.formatTime_;
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) {
var html = '';
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);
this.width_ = w;
this.height_ = h;
this.diagonal_ = diagonal;
this.vis_ = vis;
this.tip_ = tip;
this.tree_ = tree;
this.populate_();
};
/**
* Populates the tree.
*/
ImageHistoryTree.prototype.populate_ = function() {
// Build the formatted JSON block for the tree. It must be of the form:
// {
// "name": "...",
// "children": [...]
// }
var formatted = {};
var currentAncestors = this.currentImage_.ancestors.split('/');
// 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,
"highlighted": jQuery.inArray(currentAncestors, image.dbid.toString()),
"current": image.id == this.currentImage_.id
};
imageByDBID[image.dbid] = imageNode;
}
// 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 = image.ancestors.split('/');
var immediateParent = ancestors[ancestors.length - 2] * 1;
var parent = imageByDBID[immediateParent];
if (parent) {
parent.children.push(imageNode);
} else {
formatted = imageNode;
}
}
formatted.children.push({
"name": "bar"
});
this.root_ = formatted;
// Set the position of the initial node.
this.root_.x0 = this.width_ / 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.
*/
ImageHistoryTree.prototype.update_ = function(source) {
var tree = this.tree_;
var vis = this.vis_;
var diagonal = this.diagonal_;
var tip = this.tip_;
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 = d.depth * 180;
if (d.parent != null) {
d.x = d.parent.x - (d.parent.children.length-1)*30/2
+ (d.parent.children.indexOf(d))*30;
}
// if the node has too many children, go in and fix their positions to two columns.
if (d.children != null && d.children.length > 4) {
d.children.forEach(function (d, i) {
d.y = (d.depth * 180 + i % 2 * 100);
d.x = d.parent.x - (d.parent.children.length-1)*30/4
+ (d.parent.children.indexOf(d))*30/2 - i % 2 * 15;
});
}
});
// 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", function(d) {
return "node " + (d.current ? "current " : "") + (d.highlighted ? "highlighted " : "");
})
.attr("transform", function(d) { return "translate(" + source.x0 + "," + source.y0 + ")"; })
.on("click", function(d) { that.toggle_(d); that.update_(d); });
nodeEnter.append("svg:circle")
.attr("r", 1e-6)
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });
nodeEnter.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; })
.style("fill-opacity", 1e-6)
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
nodeUpdate.select("circle")
.attr("r", 4.5)
.attr("class", function(d) { return d._children ? "closed" : "open"; })
.style("fill", function(d) {
if (d.current) {
return "";
}
return d._children ? "lightsteelblue" : "#fff";
});
nodeUpdate.select("text")
.style("fill-opacity", 1);
// 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("text")
.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);
// 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;
}
};