diff options
author | Adam Spiers <git@adamspiers.org> | 2015-01-11 13:08:35 +0000 |
---|---|---|
committer | Adam Spiers <git@adamspiers.org> | 2015-01-11 13:25:46 +0000 |
commit | 9ac7713b5037710f2b1db82a63343e2f20ae775c (patch) | |
tree | d48473735eab3fd2b60ff8adee094bff2a5fb57a | |
parent | 1caef36fdeae554d5cc8bef3ca2734755bd70f08 (diff) | |
download | git-deps-9ac7713b5037710f2b1db82a63343e2f20ae775c.tar.gz |
switch to CoffeeScript!
-rw-r--r-- | README.md | 2 | ||||
-rw-r--r-- | html/js/git-deps-data.coffee | 85 | ||||
-rw-r--r-- | html/js/git-deps-data.js | 97 | ||||
-rw-r--r-- | html/js/git-deps-graph.coffee | 397 | ||||
-rw-r--r-- | html/js/git-deps-graph.js | 448 | ||||
-rw-r--r-- | html/js/git-deps-layout.coffee | 116 | ||||
-rw-r--r-- | html/js/git-deps-layout.js | 133 | ||||
-rw-r--r-- | html/js/git-deps-noty.coffee | 32 | ||||
-rw-r--r-- | html/js/git-deps-noty.js | 49 | ||||
-rw-r--r-- | html/package.json | 1 |
10 files changed, 632 insertions, 728 deletions
@@ -80,7 +80,7 @@ you will need to install some dependencies: cd html npm install - browserify -d js/git-deps-graph.js -o js/bundle.js + browserify -t coffeeify -d js/git-deps-graph.js -o js/bundle.js * You will need the [Flask](http://flask.pocoo.org/) Python module installed. diff --git a/html/js/git-deps-data.coffee b/html/js/git-deps-data.coffee new file mode 100644 index 0000000..a8b0b51 --- /dev/null +++ b/html/js/git-deps-data.coffee @@ -0,0 +1,85 @@ +$ = require("jquery") + +# The list of nodes and links to feed into WebCola. +# These will be dynamically built as we retrieve them via XHR. +nodes = [] +links = [] + +# WebCola requires links to refer to nodes by index within the +# nodes array, so as nodes are dynamically added, we need to +# be able to retrieve their index efficiently in order to add +# links to/from them. This also allows us to avoid adding the +# same node twice. +node_index = {} + +# Constraints will be added to try to keep siblings at the same y +# position. For this we need to track siblings, which we do by +# mapping each parent to an array of its siblings in this hash. +# It also enables us to deduplicate links across multiple XHRs. +deps = {} + +# Returns 1 iff a node was added, otherwise 0. +add_node = (commit) -> + return 0 if commit.sha1 of node_index + nodes.push commit + node_index[commit.sha1] = nodes.length - 1 + return 1 + +# Returns 1 iff a dependency was added, otherwise 0. +add_dependency = (parent_sha1, child_sha1) -> + deps[parent_sha1] = {} unless parent_sha1 of deps + + # We've already got this link, presumably + # from a previous XHR. + return 0 if child_sha1 of deps[parent_sha1] + deps[parent_sha1][child_sha1] = true + add_link parent_sha1, child_sha1 + return 1 + +add_link = (parent_sha1, child_sha1) -> + pi = node_index[parent_sha1] + ci = node_index[child_sha1] + link = + source: pi + target: ci + value: 1 # no idea what WebCola needs this for + + links.push link + return + +# Returns true iff new data was added. +add_data = (data) -> + new_nodes = 0 + new_deps = 0 + $.each data.commits, (i, commit) -> + new_nodes += add_node(commit) + + $.each data.dependencies, (i, dep) -> + new_deps += add_dependency(dep.parent, dep.child) + + if new_nodes > 0 or new_deps > 0 + return [ + new_nodes + new_deps + data.root + ] + + return false + +node = (sha1) -> + i = node_index[sha1] + unless i + console.error "No index for SHA1 '" + sha1 + "'" + return null + return nodes[i] + +module.exports = + # Variables + nodes: nodes + links: links + node_index: node_index + deps: deps + + # Functions + add: add_data + node: node diff --git a/html/js/git-deps-data.js b/html/js/git-deps-data.js deleted file mode 100644 index 236a35b..0000000 --- a/html/js/git-deps-data.js +++ /dev/null @@ -1,97 +0,0 @@ -var $ = require('jquery'); - -// The list of nodes and links to feed into WebCola. -// These will be dynamically built as we retrieve them via XHR. -var nodes = [], links = []; - -// WebCola requires links to refer to nodes by index within the -// nodes array, so as nodes are dynamically added, we need to -// be able to retrieve their index efficiently in order to add -// links to/from them. This also allows us to avoid adding the -// same node twice. -var node_index = {}; - -// Constraints will be added to try to keep siblings at the same y -// position. For this we need to track siblings, which we do by -// mapping each parent to an array of its siblings in this hash. -// It also enables us to deduplicate links across multiple XHRs. -var deps = {}; - -// Returns 1 iff a node was added, otherwise 0. -function add_node(commit) { - if (commit.sha1 in node_index) { - return 0; - } - nodes.push(commit); - node_index[commit.sha1] = nodes.length - 1; - return 1; -} - -// Returns 1 iff a dependency was added, otherwise 0. -function add_dependency(parent_sha1, child_sha1) { - if (! (parent_sha1 in deps)) { - deps[parent_sha1] = {}; - } - if (child_sha1 in deps[parent_sha1]) { - // We've already got this link, presumably - // from a previous XHR. - return 0; - } - - deps[parent_sha1][child_sha1] = true; - - add_link(parent_sha1, child_sha1); - - return 1; -} - -function add_link(parent_sha1, child_sha1) { - var pi = node_index[parent_sha1]; - var ci = node_index[child_sha1]; - - var link = { - source: pi, - target: ci, - value: 1 // no idea what WebCola needs this for - }; - - links.push(link); -} - -// Returns true iff new data was added. -function add_data(data) { - var new_nodes = 0, new_deps = 0; - $.each(data.commits, function (i, commit) { - new_nodes += add_node(commit); - }); - $.each(data.dependencies, function (i, dep) { - new_deps += add_dependency(dep.parent, dep.child); - }); - - if (new_nodes > 0 || new_deps > 0) { - return [new_nodes, new_deps, data.root]; - } - - return false; -} - -function node(sha1) { - var i = node_index[sha1]; - if (! i) { - console.error("No index for SHA1 '" + sha1 + "'"); - return null; - } - return nodes[i]; -} - -module.exports = { - // Variables - nodes: nodes, - links: links, - node_index: node_index, - deps: deps, - - // Functions - add: add_data, - node: node -}; diff --git a/html/js/git-deps-graph.coffee b/html/js/git-deps-graph.coffee new file mode 100644 index 0000000..558d857 --- /dev/null +++ b/html/js/git-deps-graph.coffee @@ -0,0 +1,397 @@ +jQuery = require "jquery" +$ = jQuery +d3 = require "d3" +d3tip = require "d3-tip" +d3tip d3 + +global.gdn = require "./git-deps-noty.coffee" +global.gdd = require "./git-deps-data.coffee" +global.gdl = require "./git-deps-layout.coffee" + +fullScreen = require "./fullscreen" + +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 + +svg_width = 960 +svg_height = 800 +old_svg_height = undefined +old_svg_width = undefined + +color = d3.scale.category20() + +global.d3cola = cola.d3adaptor() +d3cola \ + .flowLayout("y", 150) \ + .avoidOverlaps(true) + #.linkDistance(60) + #.symmetricDiffLinkLengths(30) + #.jaccardLinkLengths(100) + +# d3 visualization elements +container = undefined +svg = undefined +fg = undefined +node = undefined +path = undefined +tip = undefined +tip_template = undefined +zoom = undefined + +options = undefined # Options will be retrieved from web server + +jQuery -> + d3.json "options", (error, data) -> + options = data + + d3.html "tip-template.html", (error, html) -> + tip_template = html + + #setup_default_form_values(); + $("form.commitish").submit (event) -> + event.preventDefault() + add_commitish $(".commitish input").val() + +setup_default_form_values = -> + $("input[type=text]").each(-> + $(this).val $(this).attr("defaultValue") + $(this).css color: "grey" + ).focus(-> + if $(this).val() is $(this).attr("defaultValue") + $(this).val "" + $(this).css color: "black" + ).blur -> + if $(this).val() is "" + $(this).val $(this).attr("defaultValue") + $(this).css color: "grey" + +resize_window = -> + calculate_svg_size_from_container() + fit_svg_to_container() + redraw true + +redraw = (transition) -> + # if mouse down then we are dragging not panning + # if (nodeMouseDown) + # return; + ((if transition then fg.transition() else fg)) \ + .attr "transform", + "translate(" + zoom.translate() + ")" + + " scale(" + zoom.scale() + ")" + +graph_bounds = -> + x = Number.POSITIVE_INFINITY + X = Number.NEGATIVE_INFINITY + y = Number.POSITIVE_INFINITY + Y = Number.NEGATIVE_INFINITY + fg.selectAll(".node").each (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 + +fit_svg_to_container = -> + svg.attr("width", svg_width).attr("height", svg_height) + +full_screen_cancel = -> + svg_width = old_svg_width + svg_height = old_svg_height + fit_svg_to_container() + #zoom_to_fit(); + resize_window() + +full_screen_click = -> + fullScreen container[0][0], full_screen_cancel + fit_svg_to_container() + resize_window() + #zoom_to_fit(); + +zoom_to_fit = -> + b = graph_bounds() + w = b.X - b.x + h = b.Y - b.y + cw = svg.attr("width") + ch = svg.attr("height") + s = Math.min(cw / w, ch / h) + 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 + +window.full_screen_click = full_screen_click +window.zoom_to_fit = zoom_to_fit + +add_commitish = (commitish) -> + init_svg() unless svg + draw_graph commitish + +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 + +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 + +update_cola = -> + gdl.build_constraints() + d3cola \ + .nodes(gdd.nodes) \ + .links(gdd.links) \ + .constraints(gdl.constraints) + +draw_graph = (commitish) -> + d3.json "deps.json/" + commitish, (error, data) -> + if error + details = JSON.parse(error.responseText) + gdn.error details.message + return + + new_data = gdd.add(data) + + unless new_data + gdn.warn "No new commits or dependencies found!" + return + new_data_notification new_data + + update_cola() + + path = fg.selectAll(".link") \ + .data(gdd.links, link_key) + path.enter().append("svg:path") \ + .attr("class", "link") + node = fg.selectAll(".node") \ + .data(gdd.nodes, (d) -> d.sha1) \ + .call(d3cola.drag) + global.node = node + + node.enter().append("g") \ + .attr("class", "node") + # Failed attempt to use dagre layout as starting positions + # https://github.com/tgdwyer/WebCola/issues/63 + # .each(function (d, i) { + # var n = gdl.node(d.sha1); + # d.x = n.x; + # d.y = n.y; + # }); + + draw_nodes fg, node + +# Required for object constancy: http://bost.ocks.org/mike/constancy/ ... +link_key = (link) -> + source = sha1_of_link_pointer(link.source) + target = sha1_of_link_pointer(link.target) + return source + " " + target + +# ... 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. +sha1_of_link_pointer = (pointer) -> + return pointer.sha1 if typeof (pointer) is "object" + return gdd.nodes[pointer].sha1 + +new_data_notification = (new_data) -> + new_nodes = new_data[0] + new_deps = new_data[1] + root = new_data[2] + notification = \ + "<span class=\"commit-ref\">" + + root.commitish + + "</span> resolved as " + root.sha1 + notification += "<p>" + new_nodes + " new commit" + + ((if (new_nodes is 1) then "" else "s")) + notification += "; " + new_deps + " new " + + ((if (new_nodes is 1) then "dependency" else "dependencies")) + notification += "</p>" + + gdn.success notification + +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" + +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 + + rect = node.append("rect").attr("rx", 5).attr("ry", 5) + label = node.append("text").text((d) -> + d.name + ).each((d) -> + b = @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 + +position_nodes = (rect, label, tip) -> + rect.attr("width", (d, i) -> d.rect_width) \ + .attr("height", (d, i) -> d.rect_height) \ + .on("mouseover", tip.show) \ + .on("mouseout", tip.hide) + + # Centre label + label \ + .attr("x", (d) -> d.rect_width / 2) \ + .attr("y", (d) -> 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", () -> + # unless d3cola.avoidOverlaps + # gdd.nodes.forEach((v) -> + # v.width = v.height = 10 + # d3cola.avoidOverlaps true + # d3cola.start + +tip_html = (d) -> + fragment = $(tip_template).clone() + top = fragment.find("#fragment") + title = top.find("p.commit-title") + title.text d.title + + unless d.describe is "" + title.append " <span />" + describe = title.children().first() + describe.addClass("commit-describe commit-ref").text(d.describe) + + top.find("span.commit-author").text(d.author_name) + date = new Date(d.author_time * 1000) + top.find("time.commit-time") \ + .attr("datetime", date.toISOString()) \ + .text(date) + pre = top.find(".commit-body pre").text(d.body) + + if options.debug + # deps = gdd.deps[d.sha1] + # if deps + # sha1s = [gdd.node(sha1).name for name, bool of deps] + # top.append("<br />Dependencies: " + sha1s.join(", ")); + index = gdd.node_index[d.sha1] + debug = "<br />node index: " + index + dagre_node = gdl.g.graph.node(d.sha1) + debug += "<br />dagre: (" + dagre_node.x + ", " + dagre_node.y + ")" + top.append 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 + +translate = (x, y) -> + "translate(" + x + "," + y + ")" + +tick_handler = -> + node.each (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", (d) -> + translate d.innerBounds.x, d.innerBounds.y + + path.each (d) -> + @parentNode.insertBefore this, this if isIE() + + path.attr "d", (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 + + lineData = [ + {x: d.sourceIntersection.x, y: d.sourceIntersection.y}, + {x: d.arrowStart.x, y: d.arrowStart.y} + ] + return lineFunction lineData + +lineFunction = d3.svg.line() \ + .x((d) -> d.x) \ + .y((d) -> d.y) \ + .interpolate("linear") + +routeEdges = -> + d3cola.prepareEdgeRouting EDGE_ROUTING_MARGIN + path.attr "d", (d) -> + lineFunction d3cola.routeEdge(d) + # show visibility graph + # (g) -> + # if d.source.id == 10 and d.target.id === 11 + # g.E.forEach (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 (d) -> + @parentNode.insertBefore this, this + +isIE = -> + (navigator.appName is "Microsoft Internet Explorer") or + ((navigator.appName is "Netscape") and + ((new RegExp "Trident/.*rv:([0-9]{1,}[.0-9]{0,})") + .exec(navigator.userAgent)?)) diff --git a/html/js/git-deps-graph.js b/html/js/git-deps-graph.js deleted file mode 100644 index 1d34ebe..0000000 --- a/html/js/git-deps-graph.js +++ /dev/null @@ -1,448 +0,0 @@ -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'); -global.gdl = require('./git-deps-layout'); - -var fullScreen = 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 -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); -} - -window.full_screen_click = full_screen_click; -window.zoom_to_fit = zoom_to_fit; - -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() { - gdl.build_constraints(); - - d3cola - .nodes(gdd.nodes) - .links(gdd.links) - .constraints(gdl.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; - } - new_data_notification(new_data); - - update_cola(); - - 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); - global.node = node; - - node.enter().append("g") - .attr("class", "node"); - // Failed attempt to use dagre layout as starting positions - // https://github.com/tgdwyer/WebCola/issues/63 - // .each(function (d, i) { - // var n = gdl.node(d.sha1); - // d.x = n.x; - // d.y = n.y; - // }); - - 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 top = fragment.find("#fragment"); - var title = top.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); - } - top.find("span.commit-author").text(d.author_name); - var date = new Date(d.author_time * 1000); - top.find("time.commit-time") - .attr('datetime', date.toISOString()) - .text(date); - var pre = top.find(".commit-body pre").text(d.body); - - if (options.debug) { - // var deps = gdd.deps[d.sha1]; - // if (deps) { - // deps = Object.keys(deps); - // var sha1s = $.map(deps, function (sha1, i) { - // var node = gdd.node(sha1); - // return node.name; - // }); - // top.append("<br />Dependencies: " + sha1s.join(", ")); - // } - - var index = gdd.node_index[d.sha1]; - var debug = "<br />node index: " + index; - var dagre_node = gdl.g.graph.node(d.sha1); - debug += "<br />dagre: (" + dagre_node.x + ", " + dagre_node.y + ")"; - top.append(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 translate(x, y) { - return "translate(" + x + "," + y + ")"; -} - -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)); -} diff --git a/html/js/git-deps-layout.coffee b/html/js/git-deps-layout.coffee new file mode 100644 index 0000000..8e88dde --- /dev/null +++ b/html/js/git-deps-layout.coffee @@ -0,0 +1,116 @@ +$ = require "jquery" +dagre = require "dagre" + +gdd = require "./git-deps-data.coffee" + +# The list of constraints to feed into WebCola. +constraints = [] + +# Group nodes by row, as assigned by the y coordinates returned from +# dagre's layout(). This will map a y coordinate onto all nodes +# within that row. +row_groups = {} + +# Expose a container for externally accessible objects. We can't +# directly expose the objects themselves because the references +# change each time they're constructed. However we don't need this +# trick for the constraints arrays since we can easily empty that by +# setting length to 0. +externs = {} + +dagre_layout = -> + g = new dagre.graphlib.Graph() + externs.graph = g + + # Set an object for the graph label + g.setGraph {} + + # Default to assigning a new object as a label for each new edge. + g.setDefaultEdgeLabel -> {} + + $.each gdd.nodes, (i, node) -> + g.setNode node.sha1, + label: node.name + width: node.rect_width or 70 + height: node.rect_height or 30 + + $.each gdd.deps, (parent_sha1, children) -> + $.each children, (child_sha1, bool) -> + g.setEdge parent_sha1, child_sha1 + + dagre.layout g + return g + +dagre_row_groups = -> + g = dagre_layout() + row_groups = {} + externs.row_groups = row_groups + g.nodes().forEach (sha1) -> + x = g.node(sha1).x + y = g.node(sha1).y + row_groups[y] = [] unless y of row_groups + row_groups[y].push + sha1: sha1 + x: x + return row_groups + +build_constraints = -> + row_groups = dagre_row_groups() + + constraints.length = 0 # FIXME: only rebuild constraints which changed + + # We want alignment constraints between all nodes which dagre + # assigned the same y value. + for y of row_groups + row_nodes = row_groups[y] + + # No point having an alignment group with only one node in. + if row_nodes.length > 1 + constraints.push build_alignment_constraint(row_nodes) + + # We also need separation constraints ensuring that the + # top-to-bottom ordering assigned by dagre is preserved. Since + # all nodes within a single row are already constrained to the + # same y coordinate from above, it should be enough to only + # have separation between a single node in adjacent rows. + row_y_coords = Object.keys(row_groups).sort() + + i = 0 + while i < row_y_coords.length - 1 + upper_y = row_y_coords[i] + lower_y = row_y_coords[i + 1] + upper_node = row_groups[upper_y][0] + lower_node = row_groups[lower_y][0] + constraints.push + gap: 30 + axis: "y" + left: gdd.node_index[upper_node.sha1] + right: gdd.node_index[lower_node.sha1] + + i++ + +build_alignment_constraint = (row_nodes) -> + constraint = + axis: "y" + type: "alignment" + offsets: [] + + for i of row_nodes + node = row_nodes[i] + constraint.offsets.push + node: gdd.node_index[node.sha1] + offset: 0 + + return constraint + +node = (sha1) -> + externs.graph.node sha1 + +module.exports = + # Variables + constraints: constraints + g: externs + + # Functions + build_constraints: build_constraints + node: node diff --git a/html/js/git-deps-layout.js b/html/js/git-deps-layout.js deleted file mode 100644 index 2c06c1c..0000000 --- a/html/js/git-deps-layout.js +++ /dev/null @@ -1,133 +0,0 @@ -var $ = require('jquery'); -var dagre = require('dagre'); - -var gdd = require('./git-deps-data'); - -// The list of constraints to feed into WebCola. -var constraints = []; - -// Group nodes by row, as assigned by the y coordinates returned from -// dagre's layout(). This will map a y coordinate onto all nodes -// within that row. -var row_groups = {}; - -// Expose a container for externally accessible objects. We can't -// directly expose the objects themselves because the references -// change each time they're constructed. However we don't need this -// trick for the constraints arrays since we can easily empty that by -// setting length to 0. -var externs = {}; - -function dagre_layout() { - var g = new dagre.graphlib.Graph(); - externs.graph = g; - - // Set an object for the graph label - g.setGraph({}); - - // Default to assigning a new object as a label for each new edge. - g.setDefaultEdgeLabel(function() { return {}; }); - - $.each(gdd.nodes, function (i, node) { - g.setNode(node.sha1, { - label: node.name, - width: node.rect_width || 70, - height: node.rect_height || 30 - }); - }); - - $.each(gdd.deps, function (parent_sha1, children) { - $.each(children, function (child_sha1, bool) { - g.setEdge(parent_sha1, child_sha1); - }); - }); - - dagre.layout(g); - - return g; -} - -function dagre_row_groups() { - var g = dagre_layout(); - - var row_groups = {}; - externs.row_groups = row_groups; - - g.nodes().forEach(function (sha1) { - var x = g.node(sha1).x; - var y = g.node(sha1).y; - if (! (y in row_groups)) { - row_groups[y] = []; - } - row_groups[y].push({ - sha1: sha1, - x: x - }); - }); - return row_groups; -} - -function build_constraints() { - var row_groups = dagre_row_groups(); - - constraints.length = 0; // FIXME: only rebuild constraints which changed - - // We want alignment constraints between all nodes which dagre - // assigned the same y value. - for (var y in row_groups) { - var row_nodes = row_groups[y]; - // No point having an alignment group with only one node in. - if (row_nodes.length > 1) { - constraints.push(build_alignment_constraint(row_nodes)); - } - } - - // We also need separation constraints ensuring that the - // top-to-bottom ordering assigned by dagre is preserved. Since - // all nodes within a single row are already constrained to the - // same y coordinate from above, it should be enough to only - // have separation between a single node in adjacent rows. - var row_y_coords = Object.keys(row_groups).sort(); - for (var i = 0; i < row_y_coords.length - 1; i++) { - var upper_y = row_y_coords[i]; - var lower_y = row_y_coords[i+1]; - var upper_node = row_groups[upper_y][0]; - var lower_node = row_groups[lower_y][0]; - constraints.push({ - gap: 30, - axis: 'y', - left: gdd.node_index[upper_node.sha1], - right: gdd.node_index[lower_node.sha1] - }); - } -} - -function build_alignment_constraint(row_nodes) { - constraint = { - axis: 'y', - type: 'alignment', - offsets: [] - }; - for (var i in row_nodes) { - var node = row_nodes[i]; - constraint.offsets.push({ - node: gdd.node_index[node.sha1], - offset: 0 - }); - } - return constraint; -} - -function node(sha1) { - return externs.graph.node(sha1); -} - -module.exports = { - // Variables - constraints: constraints, - g: externs, - - // Functions - build_constraints: build_constraints, - node: node -}; diff --git a/html/js/git-deps-noty.coffee b/html/js/git-deps-noty.coffee new file mode 100644 index 0000000..c7e125e --- /dev/null +++ b/html/js/git-deps-noty.coffee @@ -0,0 +1,32 @@ +require "noty" + +# Different noty types: +# alert, success, error, warning, information, confirmation +noty_error = (text) -> notyfication "error", text +noty_warn = (text) -> notyfication "warning", text +noty_success = (text) -> notyfication "success", text +noty_info = (text) -> notyfication "information", text +noty_debug = (text) -> notyfication "information", text + +# "notyfication" - haha, did you see what I did there? +notyfication = (type, text) -> + window.noty( + text: text + type: type + layout: "topRight" + theme: "relax" + maxVisible: 15 + timeout: 30000 # ms + animation: + open: "animated bounceInUp" # Animate.css class names + close: "animated bounceOutUp" # Animate.css class names + easing: "swing" # unavailable - no need + speed: 500 # unavailable - no need + ) + +module.exports = + error: noty_error + warn: noty_warn + success: noty_success + info: noty_info + debug: noty_debug diff --git a/html/js/git-deps-noty.js b/html/js/git-deps-noty.js deleted file mode 100644 index 56f0b53..0000000 --- a/html/js/git-deps-noty.js +++ /dev/null @@ -1,49 +0,0 @@ -require('noty'); - -// Different noty types: -// alert, success, error, warning, information, confirmation -function noty_error(text) { - notyfication('error', text); -} - -function noty_warn(text) { - notyfication('warning', text); -} - -function noty_success(text) { - notyfication('success', text); -} - -function noty_info(text) { - notyfication('information', text); -} - -function noty_debug(text) { - notyfication('information', text); -} - -// Haha, did you see what I did here? -function notyfication(type, text) { - var n = window.noty({ - text: text, - type: type, - layout: 'topRight', - theme: 'relax', - maxVisible: 15, - timeout: 30000, // ms - animation: { - open: 'animated bounceInUp', // Animate.css class names - close: 'animated bounceOutUp', // Animate.css class names - easing: 'swing', // unavailable - no need - speed: 500 // unavailable - no need - } - }); -} - -module.exports = { - error: noty_error, - warn: noty_warn, - success: noty_success, - info: noty_info, - debug: noty_debug -}; diff --git a/html/package.json b/html/package.json index 43cdf4d..a9bf4f3 100644 --- a/html/package.json +++ b/html/package.json @@ -31,6 +31,7 @@ "jquery": "~2.1.3", "noty": "aspiers/noty#fix/commonjs", "browserify": "*", + "coffeeify" : "~1.0.0", "dagre": "~0.7.1" }, "devDependencies": { |