summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/network/branch-graph.js.coffee
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/network/branch-graph.js.coffee')
-rw-r--r--app/assets/javascripts/network/branch-graph.js.coffee340
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