diff options
Diffstat (limited to 'app/assets/javascripts/network/branch-graph.js.coffee')
-rw-r--r-- | app/assets/javascripts/network/branch-graph.js.coffee | 340 |
1 files changed, 340 insertions, 0 deletions
diff --git a/app/assets/javascripts/network/branch-graph.js.coffee b/app/assets/javascripts/network/branch-graph.js.coffee new file mode 100644 index 00000000000..f2fd2a775a4 --- /dev/null +++ b/app/assets/javascripts/network/branch-graph.js.coffee @@ -0,0 +1,340 @@ +class @BranchGraph + constructor: (@element, @options) -> + @preparedCommits = {} + @mtime = 0 + @mspace = 0 + @parents = {} + @colors = ["#000"] + @offsetX = 150 + @offsetY = 20 + @unitTime = 30 + @unitSpace = 10 + @prev_start = -1 + @load() + + load: -> + $.ajax + url: @options.url + method: "get" + dataType: "json" + success: $.proxy((data) -> + $(".loading", @element).hide() + @prepareData data.days, data.commits + @buildGraph() + , this) + + prepareData: (@days, @commits) -> + @collectParents() + @graphHeight = $(@element).height() + @graphWidth = $(@element).width() + ch = Math.max(@graphHeight, @offsetY + @unitTime * @mtime + 150) + cw = Math.max(@graphWidth, @offsetX + @unitSpace * @mspace + 300) + @r = Raphael(@element.get(0), cw, ch) + @top = @r.set() + @barHeight = Math.max(@graphHeight, @unitTime * @days.length + 320) + + for c in @commits + c.isParent = true if c.id of @parents + @preparedCommits[c.id] = c + @markCommit(c) + + @collectColors() + + collectParents: -> + for c in @commits + @mtime = Math.max(@mtime, c.time) + @mspace = Math.max(@mspace, c.space) + for p in c.parents + @parents[p[0]] = true + @mspace = Math.max(@mspace, p[1]) + + collectColors: -> + k = 0 + while k < @mspace + @colors.push Raphael.getColor(.8) + # Skipping a few colors in the spectrum to get more contrast between colors + Raphael.getColor() + Raphael.getColor() + k++ + + buildGraph: -> + r = @r + cuday = 0 + cumonth = "" + + r.rect(0, 0, 40, @barHeight).attr fill: "#222" + r.rect(40, 0, 30, @barHeight).attr fill: "#444" + + for day, mm in @days + if cuday isnt day[0] || cumonth isnt day[1] + # Dates + r.text(55, @offsetY + @unitTime * mm, day[0]) + .attr( + font: "12px Monaco, monospace" + fill: "#BBB" + ) + cuday = day[0] + + if cumonth isnt day[1] + # Months + r.text(20, @offsetY + @unitTime * mm, day[1]) + .attr( + font: "12px Monaco, monospace" + fill: "#EEE" + ) + cumonth = day[1] + + @renderPartialGraph() + + @bindEvents() + + renderPartialGraph: -> + start = Math.floor((@element.scrollTop() - @offsetY) / @unitTime) - 10 + if start < 0 + isGraphEdge = true + start = 0 + end = start + 40 + if @commits.length < end + isGraphEdge = true + end = @commits.length + + if @prev_start == -1 or Math.abs(@prev_start - start) > 10 or isGraphEdge + i = start + + @prev_start = start + + while i < end + commit = @commits[i] + i += 1 + + if commit.hasDrawn isnt true + x = @offsetX + @unitSpace * (@mspace - commit.space) + y = @offsetY + @unitTime * commit.time + + @drawDot(x, y, commit) + + @drawLines(x, y, commit) + + @appendLabel(x, y, commit) + + @appendAnchor(x, y, commit) + + commit.hasDrawn = true + + @top.toFront() + + bindEvents: -> + element = @element + + $(element).scroll (event) => + @renderPartialGraph() + + scrollDown: => + @element.scrollTop @element.scrollTop() + 50 + @renderPartialGraph() + + scrollUp: => + @element.scrollTop @element.scrollTop() - 50 + @renderPartialGraph() + + scrollLeft: => + @element.scrollLeft @element.scrollLeft() - 50 + @renderPartialGraph() + + scrollRight: => + @element.scrollLeft @element.scrollLeft() + 50 + @renderPartialGraph() + + scrollBottom: => + @element.scrollTop @element.find('svg').height() + + scrollTop: => + @element.scrollTop 0 + + appendLabel: (x, y, commit) -> + return unless commit.refs + + r = @r + shortrefs = commit.refs + # Truncate if longer than 15 chars + shortrefs = shortrefs.substr(0, 15) + "…" if shortrefs.length > 17 + text = r.text(x + 4, y, shortrefs).attr( + "text-anchor": "start" + font: "10px Monaco, monospace" + fill: "#FFF" + title: commit.refs + ) + textbox = text.getBBox() + # Create rectangle based on the size of the textbox + rect = r.rect(x, y - 7, textbox.width + 5, textbox.height + 5, 4).attr( + fill: "#000" + "fill-opacity": .5 + stroke: "none" + ) + triangle = r.path(["M", x - 5, y, "L", x - 15, y - 4, "L", x - 15, y + 4, "Z"]).attr( + fill: "#000" + "fill-opacity": .5 + stroke: "none" + ) + + label = r.set(rect, text) + label.transform(["t", -rect.getBBox().width - 15, 0]) + + # Set text to front + text.toFront() + + appendAnchor: (x, y, commit) -> + r = @r + top = @top + options = @options + anchor = r.circle(x, y, 10).attr( + fill: "#000" + opacity: 0 + cursor: "pointer" + ).click(-> + window.open options.commit_url.replace("%s", commit.id), "_blank" + ).hover(-> + @tooltip = r.commitTooltip(x + 5, y, commit) + top.push @tooltip.insertBefore(this) + , -> + @tooltip and @tooltip.remove() and delete @tooltip + ) + top.push anchor + + drawDot: (x, y, commit) -> + r = @r + r.circle(x, y, 3).attr( + fill: @colors[commit.space] + stroke: "none" + ) + + avatar_box_x = @offsetX + @unitSpace * @mspace + 10 + avatar_box_y = y - 10 + r.rect(avatar_box_x, avatar_box_y, 20, 20).attr( + stroke: @colors[commit.space] + "stroke-width": 2 + ) + r.image(commit.author.icon, avatar_box_x, avatar_box_y, 20, 20) + r.text(@offsetX + @unitSpace * @mspace + 35, y, commit.message.split("\n")[0]).attr( + "text-anchor": "start" + font: "14px Monaco, monospace" + ) + + drawLines: (x, y, commit) -> + r = @r + for parent, i in commit.parents + parentCommit = @preparedCommits[parent[0]] + parentY = @offsetY + @unitTime * parentCommit.time + parentX1 = @offsetX + @unitSpace * (@mspace - parentCommit.space) + parentX2 = @offsetX + @unitSpace * (@mspace - parent[1]) + + # Set line color + if parentCommit.space <= commit.space + color = @colors[commit.space] + + else + color = @colors[parentCommit.space] + + # Build line shape + if parent[1] is commit.space + offset = [0, 5] + arrow = "l-2,5,4,0,-2,-5,0,5" + + else if parent[1] < commit.space + offset = [3, 3] + arrow = "l5,0,-2,4,-3,-4,4,2" + + else + offset = [-3, 3] + arrow = "l-5,0,2,4,3,-4,-4,2" + + # Start point + route = ["M", x + offset[0], y + offset[1]] + + # Add arrow if not first parent + if i > 0 + route.push(arrow) + + # Circumvent if overlap + if commit.space isnt parentCommit.space or commit.space isnt parent[1] + route.push( + "L", parentX2, y + 10, + "L", parentX2, parentY - 5, + ) + + # End point + route.push("L", parentX1, parentY) + + r + .path(route) + .attr( + stroke: color + "stroke-width": 2) + + markCommit: (commit) -> + if commit.id is @options.commit_id + r = @r + x = @offsetX + @unitSpace * (@mspace - commit.space) + y = @offsetY + @unitTime * commit.time + r.path(["M", x + 5, y, "L", x + 15, y + 4, "L", x + 15, y - 4, "Z"]).attr( + fill: "#000" + "fill-opacity": .5 + stroke: "none" + ) + # Displayed in the center + @element.scrollTop(y - @graphHeight / 2) + +Raphael::commitTooltip = (x, y, commit) -> + boxWidth = 300 + boxHeight = 200 + icon = @image(gon.relative_url_root + commit.author.icon, x, y, 20, 20) + nameText = @text(x + 25, y + 10, commit.author.name) + idText = @text(x, y + 35, commit.id) + messageText = @text(x, y + 50, commit.message) + textSet = @set(icon, nameText, idText, messageText).attr( + "text-anchor": "start" + font: "12px Monaco, monospace" + ) + nameText.attr( + font: "14px Arial" + "font-weight": "bold" + ) + + idText.attr fill: "#AAA" + @textWrap messageText, boxWidth - 50 + rect = @rect(x - 10, y - 10, boxWidth, 100, 4).attr( + fill: "#FFF" + stroke: "#000" + "stroke-linecap": "round" + "stroke-width": 2 + ) + tooltip = @set(rect, textSet) + rect.attr( + height: tooltip.getBBox().height + 10 + width: tooltip.getBBox().width + 10 + ) + + tooltip.transform ["t", 20, 20] + tooltip + +Raphael::textWrap = (t, width) -> + content = t.attr("text") + abc = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + t.attr text: abc + letterWidth = t.getBBox().width / abc.length + t.attr text: content + words = content.split(" ") + x = 0 + s = [] + + for word in words + if x + (word.length * letterWidth) > width + s.push "\n" + x = 0 + x += word.length * letterWidth + s.push word + " " + + t.attr text: s.join("") + b = t.getBBox() + h = Math.abs(b.y2) - Math.abs(b.y) + 1 + t.attr y: b.y + h |