var jQuery = require('jquery');
var $ = jQuery;
var d3 = require('d3');
var d3tip = require('d3-tip');
d3tip(d3);
global.gdn = require('./git-deps-noty');
global.gdd = require('./git-deps-data');
require('./fullscreen');
var SVG_MARGIN = 2, // space around <svg>, matching #svg-container border
RECT_MARGIN = 14, // space in between <rects>
PADDING = 5, // space in between <text> label and <rect> border
EDGE_ROUTING_MARGIN = 3;
var svg_width = 960, old_svg_width,
svg_height = 800, old_svg_height;
var color = d3.scale.category20();
global.d3cola = cola.d3adaptor();
d3cola
.flowLayout("y", 150)
.linkDistance(60)
//.symmetricDiffLinkLengths(30)
//.jaccardLinkLengths(100);
.avoidOverlaps(true);
// d3 visualization elements. Kept global to aid in-browser debugging.
var container, svg, fg, node, path, tip, tip_template;
var zoom;
// Options will be retrieved from web server
var options;
jQuery(function () {
d3.json('options', function (error, data) {
options = data;
});
d3.html('tip-template.html', function (error, html) {
tip_template = html;
});
//setup_default_form_values();
$('form.commitish').submit(function (event) {
event.preventDefault();
add_commitish($('.commitish input').val());
});
});
function setup_default_form_values() {
$('input[type=text]').each(function () {
$(this).val($(this).attr('defaultValue'));
$(this).css({color: 'grey'});
}).focus(function () {
if ($(this).val() == $(this).attr('defaultValue')){
$(this).val('');
$(this).css({color: 'black'});
}
})
.blur(function () {
if ($(this).val() == '') {
$(this).val($(this).attr('defaultValue'));
$(this).css({color: 'grey'});
}
});
}
function resize_window() {
calculate_svg_size_from_container();
fit_svg_to_container();
redraw(true);
}
function redraw(transition) {
// if mouse down then we are dragging not panning
// if (nodeMouseDown)
// return;
(transition ? fg.transition() : fg)
.attr("transform",
"translate(" + zoom.translate() + ")" +
" scale(" + zoom.scale() + ")");
}
function graph_bounds() {
var x = Number.POSITIVE_INFINITY,
X = Number.NEGATIVE_INFINITY,
y = Number.POSITIVE_INFINITY,
Y = Number.NEGATIVE_INFINITY;
fg.selectAll(".node").each(function (d) {
x = Math.min(x, d.x - d.width / 2);
y = Math.min(y, d.y - d.height / 2);
X = Math.max(X, d.x + d.width / 2);
Y = Math.max(Y, d.y + d.height / 2);
});
return { x: x, X: X, y: y, Y: Y };
}
function fit_svg_to_container() {
svg.attr("width", svg_width).attr("height", svg_height);
}
function full_screen_cancel() {
svg_width = old_svg_width;
svg_height = old_svg_height;
fit_svg_to_container();
//zoom_to_fit();
resize_window();
}
function full_screen_click() {
fullScreen(container[0][0], full_screen_cancel);
fit_svg_to_container();
resize_window();
//zoom_to_fit();
}
function zoom_to_fit() {
var b = graph_bounds();
var w = b.X - b.x, h = b.Y - b.y;
var cw = svg.attr("width"), ch = svg.attr("height");
var s = Math.min(cw / w, ch / h);
var tx = -b.x * s + (cw/s - w) * s/2,
ty = -b.y * s + (ch/s - h) * s/2;
zoom.translate([tx, ty]).scale(s);
redraw(true);
}
function add_commitish(commitish) {
if (! svg) {
init_svg();
}
draw_graph(commitish);
}
function calculate_svg_size_from_container() {
old_svg_width = svg_width;
old_svg_height = svg_height;
svg_width = container[0][0].offsetWidth - SVG_MARGIN;
svg_height = container[0][0].offsetHeight - SVG_MARGIN;
}
function init_svg() {
container = d3.select('#svg-container');
calculate_svg_size_from_container();
svg = container.append('svg')
.attr('width', svg_width)
.attr('height', svg_height);
d3cola.size([svg_width, svg_height]);
d3.select(window).on('resize', resize_window);
zoom = d3.behavior.zoom();
svg.append('rect')
.attr('class', 'background')
.attr('width', "100%")
.attr('height', "100%")
.call(zoom.on("zoom", redraw))
.on('dblclick.zoom', zoom_to_fit);
fg = svg.append('g');
define_arrow_markers(fg);
}
function update_cola() {
d3cola
.nodes(gdd.nodes)
.links(gdd.links)
.constraints(gdd.constraints);
}
function draw_graph(commitish) {
d3.json("deps.json/" + commitish, function (error, data) {
if (error) {
var details = JSON.parse(error.responseText);
gdn.error(details.message);
return;
}
var new_data = gdd.add(data);
if (! new_data) {
gdn.warn('No new commits or dependencies found!');
return;
}
update_cola();
new_data_notification(new_data);
path = fg.selectAll(".link")
.data(gdd.links, link_key);
path.enter().append('svg:path')
.attr('class', 'link');
node = fg.selectAll(".node")
.data(gdd.nodes, function (d) {
return d.sha1;
})
.call(d3cola.drag);
node.enter().append("g")
.attr("class", "node");
draw_nodes(fg, node);
});
}
// Required for object constancy: http://bost.ocks.org/mike/constancy/ ...
function link_key(link) {
var source = sha1_of_link_pointer(link.source);
var target = sha1_of_link_pointer(link.target);
var key = source + " " + target;
return key;
}
// ... but even though link sources and targets are initially fed in
// as indices into the nodes array, webcola then replaces the indices
// with references to the node objects. So we have to deal with both
// cases when ensuring we are uniquely identifying each link.
function sha1_of_link_pointer(pointer) {
if (typeof(pointer) == 'object')
return pointer.sha1;
return gdd.nodes[pointer].sha1;
}
function new_data_notification(new_data) {
var new_nodes = new_data[0];
var new_deps = new_data[1];
var root = new_data[2];
var notification =
'<span class="commit-ref">' +
root.commitish +
'</span> resolved as ' + root.sha1;
notification += "<p>" + new_nodes + " new commit" +
((new_nodes == 1) ? '' : 's');
notification += "; " + new_deps + " new " +
((new_nodes == 1) ? 'dependency' : 'dependencies');
notification += '</p>';
gdn.success(notification);
}
function define_arrow_markers(fg) {
// define arrow markers for graph links
fg.append('svg:defs').append('svg:marker')
.attr('id', 'end-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 6)
.attr('markerWidth', 8)
.attr('markerHeight', 8)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#000');
}
function draw_nodes(fg, node) {
// Initialize tooltip
tip = d3.tip().attr('class', 'd3-tip').html(tip_html);
fg.call(tip);
hide_tip_on_drag = d3cola.drag().on('dragstart', tip.hide);
node.call(hide_tip_on_drag);
var rect = node.append("rect")
.attr("rx", 5).attr("ry", 5);
var label = node.append("text")
.text(function (d) { return d.name; })
.each(function (d) {
var b = this.getBBox();
// Calculate width/height of rectangle from text bounding box.
d.rect_width = b.width + 2 * PADDING;
d.rect_height = b.height + 2 * PADDING;
// Now set the node width/height as used by cola for
// positioning. This has to include the margin
// outside the rectangle.
d.width = d.rect_width + 2 * RECT_MARGIN;
d.height = d.rect_height + 2 * RECT_MARGIN;
});
position_nodes(rect, label, tip);
}
function position_nodes(rect, label, tip) {
rect.attr('width', function (d, i) { return d.rect_width; })
.attr('height', function (d, i) { return d.rect_height; })
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
// Centre label
label
.attr("x", function (d) { return d.rect_width / 2; })
.attr("y", function (d) { return d.rect_height / 2; })
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
d3cola.start(10,20,20);
d3cola.on("tick", tick_handler);
// d3cola.on("end", routeEdges);
// turn on overlap avoidance after first convergence
// d3cola.on("end", function () {
// if (!d3cola.avoidOverlaps()) {
// gdd.nodes.forEach(function (v) {
// v.width = v.height = 10;
// });
// d3cola.avoidOverlaps(true);
// d3cola.start();
// }
// });
}
function tip_html(d) {
var fragment = $(tip_template).clone();
var title = fragment.find("p.commit-title");
title.text(d.title);
if (d.describe != "") {
title.append(" <span />");
var describe = title.children().first();
describe.addClass("commit-describe commit-ref").text(d.describe);
}
fragment.find("span.commit-author").text(d.author_name);
var date = new Date(d.author_time * 1000);
fragment.find("time.commit-time")
.attr('datetime', date.toISOString())
.text(date);
var pre = fragment.find(".commit-body pre").text(d.body);
if (options.debug) {
var index = gdd.node_index[d.sha1];
var debug = "node index: " + index;
$.each(gdd.constraints, function (i, constraint) {
if (constraint.parent == d.sha1) {
var siblings = $.map(constraint.offsets,
function (offset, i) {
return offset.node;
});
debug += "<br />constrained children: " + siblings.join(", ");
}
});
pre.after(debug);
}
// Javascript *sucks*. There's no way to get the outerHTML of a
// document fragment, so you have to wrap the whole thing in a
// single parent and then look that up via children[0].
return fragment[0].children[0].outerHTML;
}
function tick_handler() {
node.each(function (d) {
// cola sets the bounds property which is a Rectangle
// representing the space which other nodes should not
// overlap. The innerBounds property seems to tell
// cola the Rectangle which is the visible part of the
// node, minus any blank margin.
d.innerBounds = d.bounds.inflate(-RECT_MARGIN);
});
node.attr("transform", function (d) {
return "translate(" +
d.innerBounds.x + "," +
d.innerBounds.y + ")";
});
path.each(function (d) {
if (isIE()) this.parentNode.insertBefore(this, this);
});
path.attr("d", function (d) {
// Undocumented: https://github.com/tgdwyer/WebCola/issues/52
cola.vpsc.makeEdgeBetween(
d,
d.source.innerBounds,
d.target.innerBounds,
// This value is related to but not equal to the
// distance of arrow tip from object it points at:
5
);
var lineData = [
{ x: d.sourceIntersection.x, y: d.sourceIntersection.y },
{ x: d.arrowStart.x, y: d.arrowStart.y }
];
return lineFunction(lineData);
});
}
var lineFunction = d3.svg.line()
.x(function (d) { return d.x; })
.y(function (d) { return d.y; })
.interpolate("linear");
var routeEdges = function () {
d3cola.prepareEdgeRouting(EDGE_ROUTING_MARGIN);
path.attr("d", function (d) {
return lineFunction(d3cola.routeEdge(d)
// // show visibility graph
//, function (g) {
// if (d.source.id === 10 && d.target.id === 11) {
// g.E.forEach(function (e) {
// vis.append("line").attr("x1", e.source.p.x).attr("y1", e.source.p.y)
// .attr("x2", e.target.p.x).attr("y2", e.target.p.y)
// .attr("stroke", "green");
// });
// }
// }));
);
});
if (isIE()) {
path.each(function (d) {
this.parentNode.insertBefore(this, this);
});
}
};
function isIE() {
return (navigator.appName == 'Microsoft Internet Explorer') ||
((navigator.appName == 'Netscape') &&
(new RegExp("Trident/.*rv:([0-9]{1,}[\.0-9]{0,})").exec(navigator.userAgent)
!= null));
}