aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Spiers <git@adamspiers.org>2015-01-11 13:08:35 +0000
committerAdam Spiers <git@adamspiers.org>2015-01-11 13:25:46 +0000
commit9ac7713b5037710f2b1db82a63343e2f20ae775c (patch)
treed48473735eab3fd2b60ff8adee094bff2a5fb57a
parent1caef36fdeae554d5cc8bef3ca2734755bd70f08 (diff)
downloadgit-deps-9ac7713b5037710f2b1db82a63343e2f20ae775c.tar.gz
switch to CoffeeScript!
-rw-r--r--README.md2
-rw-r--r--html/js/git-deps-data.coffee85
-rw-r--r--html/js/git-deps-data.js97
-rw-r--r--html/js/git-deps-graph.coffee397
-rw-r--r--html/js/git-deps-graph.js448
-rw-r--r--html/js/git-deps-layout.coffee116
-rw-r--r--html/js/git-deps-layout.js133
-rw-r--r--html/js/git-deps-noty.coffee32
-rw-r--r--html/js/git-deps-noty.js49
-rw-r--r--html/package.json1
10 files changed, 632 insertions, 728 deletions
diff --git a/README.md b/README.md
index 84cb65a..57d57ee 100644
--- a/README.md
+++ b/README.md
@@ -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": {