branch-graph.js.coffee 8.47 KB
Newer Older
1
class @BranchGraph
Sato Hiroyuki's avatar
Sato Hiroyuki committed
2 3 4 5 6 7
  constructor: (@element, @options) ->
    @preparedCommits = {}
    @mtime = 0
    @mspace = 0
    @parents = {}
    @colors = ["#000"]
Sato Hiroyuki's avatar
Sato Hiroyuki committed
8
    @offsetX = 150
Sato Hiroyuki's avatar
Sato Hiroyuki committed
9
    @offsetY = 20
10
    @unitTime = 30
Sato Hiroyuki's avatar
Sato Hiroyuki committed
11
    @unitSpace = 10
Sato Hiroyuki's avatar
Sato Hiroyuki committed
12
    @prev_start = -1
Sato Hiroyuki's avatar
Sato Hiroyuki committed
13
    @load()
Valery Sizov's avatar
Valery Sizov committed
14

Sato Hiroyuki's avatar
Sato Hiroyuki committed
15 16 17 18 19 20 21 22 23 24
  load: ->
    $.ajax
      url: @options.url
      method: "get"
      dataType: "json"
      success: $.proxy((data) ->
        $(".loading", @element).hide()
        @prepareData data.days, data.commits
        @buildGraph()
      , this)
25

Sato Hiroyuki's avatar
Sato Hiroyuki committed
26 27
  prepareData: (@days, @commits) ->
    @collectParents()
Sato Hiroyuki's avatar
Sato Hiroyuki committed
28 29 30 31 32 33 34
    @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)
Sato Hiroyuki's avatar
Sato Hiroyuki committed
35 36 37 38

    for c in @commits
      c.isParent = true  if c.id of @parents
      @preparedCommits[c.id] = c
Sato Hiroyuki's avatar
Sato Hiroyuki committed
39
      @markCommit(c)
Sato Hiroyuki's avatar
Sato Hiroyuki committed
40 41 42 43 44 45 46 47 48

    @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
Sato Hiroyuki's avatar
Sato Hiroyuki committed
49
        @mspace = Math.max(@mspace, p[1])
Sato Hiroyuki's avatar
Sato Hiroyuki committed
50 51 52 53 54 55 56 57 58 59 60

  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: ->
Sato Hiroyuki's avatar
Sato Hiroyuki committed
61
    r = @r
Sato Hiroyuki's avatar
Sato Hiroyuki committed
62 63
    cuday = 0
    cumonth = ""
Sato Hiroyuki's avatar
Sato Hiroyuki committed
64

65 66
    r.rect(0, 0, 40, @barHeight).attr fill: "#222"
    r.rect(40, 0, 30, @barHeight).attr fill: "#444"
Sato Hiroyuki's avatar
Sato Hiroyuki committed
67 68

    for day, mm in @days
69
      if cuday isnt day[0] || cumonth isnt day[1]
Sato Hiroyuki's avatar
Sato Hiroyuki committed
70
        # Dates
71
        r.text(55, @offsetY + @unitTime * mm, day[0])
Sato Hiroyuki's avatar
Sato Hiroyuki committed
72 73
          .attr(
            font: "12px Monaco, monospace"
74
            fill: "#BBB"
Sato Hiroyuki's avatar
Sato Hiroyuki committed
75 76 77 78 79
          )
        cuday = day[0]

      if cumonth isnt day[1]
        # Months
80
        r.text(20, @offsetY + @unitTime * mm, day[1])
Sato Hiroyuki's avatar
Sato Hiroyuki committed
81 82
          .attr(
            font: "12px Monaco, monospace"
83
            fill: "#EEE"
Sato Hiroyuki's avatar
Sato Hiroyuki committed
84 85 86
          )
        cumonth = day[1]

Sato Hiroyuki's avatar
Sato Hiroyuki committed
87
    @renderPartialGraph()
Sato Hiroyuki's avatar
Sato Hiroyuki committed
88

Sato Hiroyuki's avatar
Sato Hiroyuki committed
89
    @bindEvents()
Sato Hiroyuki's avatar
Sato Hiroyuki committed
90

Sato Hiroyuki's avatar
Sato Hiroyuki committed
91 92
  renderPartialGraph: ->
    start = Math.floor((@element.scrollTop() - @offsetY) / @unitTime) - 10
93 94 95
    if start < 0
      isGraphEdge = true
      start = 0
Sato Hiroyuki's avatar
Sato Hiroyuki committed
96
    end = start + 40
97 98 99
    if @commits.length < end
      isGraphEdge = true
      end = @commits.length
Sato Hiroyuki's avatar
Sato Hiroyuki committed
100

101
    if @prev_start == -1 or Math.abs(@prev_start - start) > 10 or isGraphEdge
Sato Hiroyuki's avatar
Sato Hiroyuki committed
102
      i = start
103

Sato Hiroyuki's avatar
Sato Hiroyuki committed
104
      @prev_start = start
105

Sato Hiroyuki's avatar
Sato Hiroyuki committed
106 107 108
      while i < end
        commit = @commits[i]
        i += 1
Sato Hiroyuki's avatar
Sato Hiroyuki committed
109

Sato Hiroyuki's avatar
Sato Hiroyuki committed
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
        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()
Sato Hiroyuki's avatar
Sato Hiroyuki committed
125 126 127

  bindEvents: ->
    element = @element
128 129 130

    $(element).scroll (event) =>
      @renderPartialGraph()
Sato Hiroyuki's avatar
Sato Hiroyuki committed
131

132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
  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
Sato Hiroyuki's avatar
Sato Hiroyuki committed
153 154 155

  appendLabel: (x, y, commit) ->
    return unless commit.refs
Sato Hiroyuki's avatar
Sato Hiroyuki committed
156

Sato Hiroyuki's avatar
Sato Hiroyuki committed
157
    r = @r
Sato Hiroyuki's avatar
Sato Hiroyuki committed
158
    shortrefs = commit.refs
Sato Hiroyuki's avatar
Sato Hiroyuki committed
159 160
    # Truncate if longer than 15 chars
    shortrefs = shortrefs.substr(0, 15) + "…"  if shortrefs.length > 17
Sato Hiroyuki's avatar
Sato Hiroyuki committed
161
    text = r.text(x + 4, y, shortrefs).attr(
Sato Hiroyuki's avatar
Sato Hiroyuki committed
162
      "text-anchor": "start"
Sato Hiroyuki's avatar
Sato Hiroyuki committed
163 164
      font: "10px Monaco, monospace"
      fill: "#FFF"
Sato Hiroyuki's avatar
Sato Hiroyuki committed
165
      title: commit.refs
Sato Hiroyuki's avatar
Sato Hiroyuki committed
166 167 168
    )
    textbox = text.getBBox()
    # Create rectangle based on the size of the textbox
Sato Hiroyuki's avatar
Sato Hiroyuki committed
169
    rect = r.rect(x, y - 7, textbox.width + 5, textbox.height + 5, 4).attr(
Sato Hiroyuki's avatar
Sato Hiroyuki committed
170
      fill: "#000"
Sato Hiroyuki's avatar
Sato Hiroyuki committed
171
      "fill-opacity": .5
Sato Hiroyuki's avatar
Sato Hiroyuki committed
172 173
      stroke: "none"
    )
Sato Hiroyuki's avatar
Sato Hiroyuki committed
174
    triangle = r.path(["M", x - 5, y, "L", x - 15, y - 4, "L", x - 15, y + 4, "Z"]).attr(
Sato Hiroyuki's avatar
Sato Hiroyuki committed
175
      fill: "#000"
Sato Hiroyuki's avatar
Sato Hiroyuki committed
176
      "fill-opacity": .5
Sato Hiroyuki's avatar
Sato Hiroyuki committed
177 178
      stroke: "none"
    )
Sato Hiroyuki's avatar
Sato Hiroyuki committed
179 180 181 182

    label = r.set(rect, text)
    label.transform(["t", -rect.getBBox().width - 15, 0])

Sato Hiroyuki's avatar
Sato Hiroyuki committed
183 184
    # Set text to front
    text.toFront()
Koen Punt's avatar
Koen Punt committed
185

Sato Hiroyuki's avatar
Sato Hiroyuki committed
186
  appendAnchor: (x, y, commit) ->
Sato Hiroyuki's avatar
Sato Hiroyuki committed
187
    r = @r
Sato Hiroyuki's avatar
Sato Hiroyuki committed
188
    top = @top
Sato Hiroyuki's avatar
Sato Hiroyuki committed
189 190 191 192
    options = @options
    anchor = r.circle(x, y, 10).attr(
      fill: "#000"
      opacity: 0
Koen Punt's avatar
Koen Punt committed
193
      cursor: "pointer"
Sato Hiroyuki's avatar
Sato Hiroyuki committed
194 195 196
    ).click(->
      window.open options.commit_url.replace("%s", commit.id), "_blank"
    ).hover(->
Sato Hiroyuki's avatar
Sato Hiroyuki committed
197
      @tooltip = r.commitTooltip(x + 5, y, commit)
Sato Hiroyuki's avatar
Sato Hiroyuki committed
198 199 200 201 202 203
      top.push @tooltip.insertBefore(this)
    , ->
      @tooltip and @tooltip.remove() and delete @tooltip
    )
    top.push anchor

Sato Hiroyuki's avatar
Sato Hiroyuki committed
204
  drawDot: (x, y, commit) ->
Sato Hiroyuki's avatar
Sato Hiroyuki committed
205
    r = @r
Sato Hiroyuki's avatar
Sato Hiroyuki committed
206 207 208 209
    r.circle(x, y, 3).attr(
      fill: @colors[commit.space]
      stroke: "none"
    )
Jeroen van Baarsen's avatar
Jeroen van Baarsen committed
210 211 212 213

    avatar_box_x = @offsetX + @unitSpace * @mspace + 10
    avatar_box_y = y - 10
    r.rect(avatar_box_x, avatar_box_y, 20, 20).attr(
214 215 216
      stroke: @colors[commit.space]
      "stroke-width": 2
    )
217
    r.image(commit.author.icon, avatar_box_x, avatar_box_y, 20, 20)
218 219 220 221
    r.text(@offsetX + @unitSpace * @mspace + 35, y, commit.message.split("\n")[0]).attr(
      "text-anchor": "start"
      font: "14px Monaco, monospace"
    )
Sato Hiroyuki's avatar
Sato Hiroyuki committed
222 223

  drawLines: (x, y, commit) ->
Sato Hiroyuki's avatar
Sato Hiroyuki committed
224
    r = @r
225
    for parent, i in commit.parents
Sato Hiroyuki's avatar
Sato Hiroyuki committed
226
      parentCommit = @preparedCommits[parent[0]]
Sato Hiroyuki's avatar
Sato Hiroyuki committed
227 228 229
      parentY = @offsetY + @unitTime * parentCommit.time
      parentX1 = @offsetX + @unitSpace * (@mspace - parentCommit.space)
      parentX2 = @offsetX + @unitSpace * (@mspace - parent[1])
Sato Hiroyuki's avatar
Sato Hiroyuki committed
230

231 232 233 234 235 236 237 238 239
      # 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
Sato Hiroyuki's avatar
Sato Hiroyuki committed
240 241
        offset = [0, 5]
        arrow = "l-2,5,4,0,-2,-5,0,5"
Sato Hiroyuki's avatar
Sato Hiroyuki committed
242

243
      else if parent[1] < commit.space
Sato Hiroyuki's avatar
Sato Hiroyuki committed
244 245
        offset = [3, 3]
        arrow = "l5,0,-2,4,-3,-4,4,2"
Sato Hiroyuki's avatar
Sato Hiroyuki committed
246 247

      else
Sato Hiroyuki's avatar
Sato Hiroyuki committed
248 249
        offset = [-3, 3]
        arrow = "l-5,0,2,4,3,-4,-4,2"
250 251

      # Start point
Sato Hiroyuki's avatar
Sato Hiroyuki committed
252
      route = ["M", x + offset[0], y + offset[1]]
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272

      # 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)
Sato Hiroyuki's avatar
Sato Hiroyuki committed
273

Sato Hiroyuki's avatar
Sato Hiroyuki committed
274
  markCommit: (commit) ->
Sato Hiroyuki's avatar
Sato Hiroyuki committed
275
    if commit.id is @options.commit_id
Sato Hiroyuki's avatar
Sato Hiroyuki committed
276
      r = @r
Sato Hiroyuki's avatar
Sato Hiroyuki committed
277 278
      x = @offsetX + @unitSpace * (@mspace - commit.space)
      y = @offsetY + @unitTime * commit.time
Sato Hiroyuki's avatar
Sato Hiroyuki committed
279
      r.path(["M", x + 5, y, "L", x + 15, y + 4, "L", x + 15, y - 4, "Z"]).attr(
Sato Hiroyuki's avatar
Sato Hiroyuki committed
280
        fill: "#000"
Sato Hiroyuki's avatar
Sato Hiroyuki committed
281
        "fill-opacity": .5
Sato Hiroyuki's avatar
Sato Hiroyuki committed
282 283 284
        stroke: "none"
      )
      # Displayed in the center
Sato Hiroyuki's avatar
Sato Hiroyuki committed
285
      @element.scrollTop(y - @graphHeight / 2)
Sato Hiroyuki's avatar
Sato Hiroyuki committed
286

Sato Hiroyuki's avatar
Sato Hiroyuki committed
287 288 289
Raphael::commitTooltip = (x, y, commit) ->
  boxWidth = 300
  boxHeight = 200
290
  icon = @image(gon.relative_url_root + commit.author.icon, x, y, 20, 20)
Sato Hiroyuki's avatar
Sato Hiroyuki committed
291 292 293 294 295 296 297 298 299
  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"
Koen Punt's avatar
Koen Punt committed
300
    "font-weight": "bold"
Sato Hiroyuki's avatar
Sato Hiroyuki committed
301
  )
Koen Punt's avatar
Koen Punt committed
302

Sato Hiroyuki's avatar
Sato Hiroyuki committed
303 304 305 306 307 308
  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"
Koen Punt's avatar
Koen Punt committed
309
    "stroke-width": 2
Sato Hiroyuki's avatar
Sato Hiroyuki committed
310 311 312 313 314 315 316 317 318
  )
  tooltip = @set(rect, textSet)
  rect.attr(
    height: tooltip.getBBox().height + 10
    width: tooltip.getBBox().width + 10
  )

  tooltip.transform ["t", 20, 20]
  tooltip
Koen Punt's avatar
Koen Punt committed
319

Sato Hiroyuki's avatar
Sato Hiroyuki committed
320 321 322 323 324 325 326 327 328
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 = []
Koen Punt's avatar
Koen Punt committed
329

Sato Hiroyuki's avatar
Sato Hiroyuki committed
330 331 332 333 334 335
  for word in words
    if x + (word.length * letterWidth) > width
      s.push "\n"
      x = 0
    x += word.length * letterWidth
    s.push word + " "
Koen Punt's avatar
Koen Punt committed
336

Sato Hiroyuki's avatar
Sato Hiroyuki committed
337 338 339 340
  t.attr text: s.join("")
  b = t.getBBox()
  h = Math.abs(b.y2) - Math.abs(b.y) + 1
  t.attr y: b.y + h