branch-graph.js 10.2 KB
Newer Older
1
!function(){
Valery Sizov's avatar
Valery Sizov committed
2

3
  var BranchGraph = function(element, options){
Koen Punt's avatar
Koen Punt committed
4
    this.element = element;
5
    this.options = options;
6
    
7
    this.preparedCommits = {};
8 9 10 11 12
    this.mtime = 0;
    this.mspace = 0;
    this.parents = {};
    this.colors = ["#000"];
    
Koen Punt's avatar
Koen Punt committed
13
    this.load();
14 15
  };
  
Koen Punt's avatar
Koen Punt committed
16 17
  BranchGraph.prototype.load = function(){
    $.ajax({
18
      url: this.options.url,
Koen Punt's avatar
Koen Punt committed
19 20 21 22 23
      method: 'get',
      dataType: 'json',
      success: $.proxy(function(data){
        $('.loading', this.element).hide();
        this.prepareData(data.days, data.commits);
24
        this.buildGraph();
Koen Punt's avatar
Koen Punt committed
25 26
      }, this)
    });
27
  };
Koen Punt's avatar
Koen Punt committed
28 29 30
  
  BranchGraph.prototype.prepareData = function(days, commits){
    this.days = days;
31
    this.dayCount = days.length;
Koen Punt's avatar
Koen Punt committed
32
    this.commits = commits;
33 34 35 36
    this.commitCount = commits.length;
    
    this.collectParents();
    
Koen Punt's avatar
Koen Punt committed
37 38
    this.mtime += 4;
    this.mspace += 10;
39
    for (var i = 0; i < this.commitCount; i++) {
40 41
      if (this.commits[i].id in this.parents) {
        this.commits[i].isParent = true;
Valery Sizov's avatar
Valery Sizov committed
42
      }
43
      this.preparedCommits[this.commits[i].id] = this.commits[i];
44
    }
45 46 47 48 49 50 51
    this.collectColors();
  };
  
  BranchGraph.prototype.collectParents = function(){
    for (var i = 0; i < this.commitCount; i++) {
      for (var j = 0, jj = this.commits[i].parents.length; j < jj; j++) {
        this.parents[this.commits[i].parents[j][0]] = true;
Valery Sizov's avatar
Valery Sizov committed
52
      }
53 54 55 56 57 58
      this.mtime = Math.max(this.mtime, this.commits[i].time);
      this.mspace = Math.max(this.mspace, this.commits[i].space);
    }
  };
  
  BranchGraph.prototype.collectColors = function(){
59
    for (var k = 0; k < this.mspace; k++) {
60 61 62
      this.colors.push(Raphael.getColor(.8));
      // Skipping a few colors in the spectrum to get more contrast between colors
      Raphael.getColor();Raphael.getColor();
63 64
    }
  };
65

66 67
  BranchGraph.prototype.buildGraph = function(){
    var graphWidth = $(this.element).width()
68 69
      , ch = this.mspace * 20 + 100
      , cw = Math.max(graphWidth, this.mtime * 20 + 260)
70
      , r = Raphael(this.element.get(0), cw, ch)
71 72
      , top = r.set()
      , cuday = 0
Koen Punt's avatar
Koen Punt committed
73
      , cumonth = ""
74 75
      , offsetX = 20
      , offsetY = 60
76 77
      , barWidth = Math.max(graphWidth, this.dayCount * 20 + 320)
      , scrollLeft = cw;
Koen Punt's avatar
Koen Punt committed
78 79
    
    this.raphael = r;
80
    
81 82
    r.rect(0, 0, barWidth, 20).attr({fill: "#222"});
    r.rect(0, 20, barWidth, 20).attr({fill: "#444"});
83
    
84
    for (mm = 0; mm < this.dayCount; mm++) {
85 86
      if(this.days[mm] != null){
        if(cuday != this.days[mm][0]){
87 88
          // Dates
          r.text(offsetX + mm * 20, 31, this.days[mm][0]).attr({
89
            font: "12px Monaco, monospace",
90 91 92
            fill: "#DDD"
          });
          cuday = this.days[mm][0];
Valery Sizov's avatar
Valery Sizov committed
93
        }
94
        if(cumonth != this.days[mm][1]){
95 96
          // Months
          r.text(offsetX + mm * 20, 11, this.days[mm][1]).attr({
97
            font: "12px Monaco, monospace", 
98 99 100
            fill: "#EEE"
          });
          cumonth = this.days[mm][1];
Valery Sizov's avatar
Valery Sizov committed
101
        }
102
      }
Valery Sizov's avatar
Valery Sizov committed
103
    }
104
    
105 106
    for (i = 0; i < this.commitCount; i++) {
      var x = offsetX + 20 * this.commits[i].time
107 108 109
        , y = offsetY + 10 * this.commits[i].space
        , c
        , ps;
110 111
      
      // Draw dot
112 113 114 115
      r.circle(x, y, 3).attr({
        fill: this.colors[this.commits[i].space], 
        stroke: "none"
      });
Koen Punt's avatar
Koen Punt committed
116
      
117
      // Draw lines
118
      for (var j = 0, jj = this.commits[i].parents.length; j < jj; j++) {
119
        c = this.preparedCommits[this.commits[i].parents[j][0]];
120
        ps = this.commits[i].parents[j][1];
121 122 123 124 125 126 127 128 129 130 131
        var cx = offsetX + 20 * c.time
          , cy = offsetY + 10 * c.space
          , psy = offsetY + 10 * ps;
        if (c.space == this.commits[i].space && c.space == ps) {
          r.path([
            "M", x, y,
            "L", cx, cy
          ]).attr({
            stroke: this.colors[c.space], 
            "stroke-width": 2
          });
132

133 134 135 136 137 138 139 140 141 142 143 144 145
        } else if (c.space < this.commits[i].space) {
          if (y == psy) {
              r.path([
                  "M", x - 5, y,
                  "l-5,-2,0,4,5,-2",
                  "L", x - 10, y,
                  "L", x - 15, psy,
                  "L", cx + 5, psy,
                  "L", cx, cy])
              .attr({
                stroke: this.colors[this.commits[i].space], 
                "stroke-width": 2
              });
146
          } else {
147 148 149 150 151 152 153 154 155 156 157
              r.path([
                  "M", x - 3, y - 6,
                  "l-4,-3,4,-2,0,5",
                  "L", x - 5, y - 10,
                  "L", x - 10, psy,
                  "L", cx + 5, psy,
                  "L", cx, cy])
              .attr({
                stroke: this.colors[this.commits[i].space], 
                "stroke-width": 2
              });
158
          }
159 160 161 162 163 164 165 166 167 168 169 170
        } else {
          r.path([
              "M", x - 3, y + 6,
              "l-4,3,4,2,0,-5",
              "L", x - 5, y + 10,
              "L", x - 10, psy,
              "L", cx + 5, psy,
              "L", cx, cy])
          .attr({
            stroke: this.colors[c.space], 
            "stroke-width": 2
          });
Valery Sizov's avatar
Valery Sizov committed
171
        }
172
      }
173 174 175 176 177
      
      if (this.commits[i].refs) {
        this.appendLabel(x, y, this.commits[i].refs);
      }
      
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
      // mark commit and displayed in the center
      if (this.commits[i].id == this.options.commit_id) {
        r.path([
            'M', x, y - 5,
            'L', x + 4, y - 15,
            'L', x - 4, y - 15,
            'Z'
            ]).attr({
              "fill": "#000",
              "fill-opacity": .7,
              "stroke": "none"
              });
        scrollLeft = x - graphWidth / 2;
      }

Koen Punt's avatar
Koen Punt committed
193
      this.appendAnchor(top, this.commits[i], x, y);
Valery Sizov's avatar
Valery Sizov committed
194 195
    }
    top.toFront();
196
    this.element.scrollLeft(scrollLeft);
197 198 199 200 201 202
    this.bindEvents();
  };
  
  BranchGraph.prototype.bindEvents = function(){
    var drag = {}
      , element = this.element;
Koen Punt's avatar
Koen Punt committed
203
      
204 205 206
    var dragger = function(event){
      element.scrollLeft(drag.sl - (event.clientX - drag.x));
      element.scrollTop(drag.st - (event.clientY - drag.y));
Valery Sizov's avatar
Valery Sizov committed
207
    };
208 209 210 211 212 213 214 215 216 217
    
    element.on({
      mousedown: function (event) {
        drag = {
          x: event.clientX, 
          y: event.clientY, 
          st: element.scrollTop(), 
          sl: element.scrollLeft()
        };
        $(window).on('mousemove', dragger);
Koen Punt's avatar
Koen Punt committed
218
      }
219 220 221 222 223 224 225 226 227 228
    });
    $(window).on({
      mouseup: function(){
        //bars.animate({opacity: 0}, 300);
        $(window).off('mousemove', dragger);
      },
      keydown: function(event){
        if(event.keyCode == 37){
          // left
          element.scrollLeft( element.scrollLeft() - 50); 
Valery Sizov's avatar
Valery Sizov committed
229
        }
230 231 232 233 234 235 236 237 238 239 240 241
        if(event.keyCode == 38){
          // top
          element.scrollTop( element.scrollTop() - 50); 
        }
        if(event.keyCode == 39){
          // right
          element.scrollLeft( element.scrollLeft() + 50); 
        }
        if(event.keyCode == 40){
          // bottom
          element.scrollTop( element.scrollTop() + 50); 
        }
Koen Punt's avatar
Koen Punt committed
242 243 244 245
      }
    });
  };
  
Koen Punt's avatar
Koen Punt committed
246 247 248 249 250
  BranchGraph.prototype.appendLabel = function(x, y, refs){
    var r = this.raphael
      , shortrefs = refs
      , text, textbox, rect;
    
251
    if (shortrefs.length > 17){
Koen Punt's avatar
Koen Punt committed
252
      // Truncate if longer than 15 chars
253
      shortrefs = shortrefs.substr(0,15) + "";
Koen Punt's avatar
Koen Punt committed
254 255
    }
    
256 257
    text = r.text(x+5, y+8 + 10, shortrefs).attr({
      font: "10px Monaco, monospace", 
Koen Punt's avatar
Koen Punt committed
258 259 260 261 262 263 264 265 266 267 268 269
      fill: "#FFF",
      title: refs
    });

    textbox = text.getBBox();
    text.transform([
      't', textbox.height/-4, textbox.width/2 + 5,
      'r90'
    ]);
    
    // Create rectangle based on the size of the textbox
    rect = r.rect(x, y, textbox.width + 15, textbox.height + 5, 4).attr({
270 271 272 273 274 275 276 277 278 279 280 281 282 283
      "fill": "#000",
      "fill-opacity": .7,
      "stroke": "none"
    });
    
    triangle = r.path([
      'M', x, y + 5,
      'L', x + 4, y + 15,
      'L', x - 4, y + 15,
      'Z'
    ]).attr({
      "fill": "#000",
      "fill-opacity": .7,
      "stroke": "none"
Koen Punt's avatar
Koen Punt committed
284 285 286 287 288
    });
    
    // Rotate and reposition rectangle over text
    rect.transform([
      'r', 90, x, y,
289
      't', 15, -9
Koen Punt's avatar
Koen Punt committed
290 291 292 293 294 295
    ]);
    
    // Set text to front
    text.toFront();
  };
  
296
  BranchGraph.prototype.appendAnchor = function(top, commit, x, y) {
297 298 299 300
    var r = this.raphael
      , options = this.options
      , anchor;
    anchor = r.circle(x, y, 10).attr({
Koen Punt's avatar
Koen Punt committed
301 302 303 304 305
      fill: "#000", 
      opacity: 0, 
      cursor: "pointer"
    })
    .click(function(){
306
      window.open(options.commit_url.replace('%s', commit.id), '_blank');
Koen Punt's avatar
Koen Punt committed
307
    })
308
    .hover(function(){
309 310
      this.tooltip = r.commitTooltip(x, y + 5, commit);
      top.push(this.tooltip.insertBefore(this));
311
    }, function(){
312
      this.tooltip && this.tooltip.remove() && delete this.tooltip;
313 314
    });
    top.push(anchor);
315 316 317 318 319
  };
  
  this.BranchGraph = BranchGraph;
  
}(this);
320
Raphael.fn.commitTooltip = function(x, y, commit){
321
  var icon, nameText, idText, messageText
322 323 324
    , boxWidth = 300
    , boxHeight = 200;
  
325 326
  icon = this.image(commit.author.icon, x, y, 20, 20);
  nameText = this.text(x + 25, y + 10, commit.author.name);
327 328 329
  idText = this.text(x, y + 35, commit.id);
  messageText = this.text(x, y + 50, commit.message);
  
330
  textSet = this.set(icon, nameText, idText, messageText).attr({
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363
    "text-anchor": "start",
    "font": "12px Monaco, monospace"
  });
  
  nameText.attr({
    "font": "14px Arial",
    "font-weight": "bold"
  });
  
  idText.attr({
    "fill": "#AAA"
  });
  
  textWrap(messageText, boxWidth - 50);

  var rect = this.rect(x - 10, y - 10, boxWidth, 100, 4).attr({
    "fill": "#FFF",
    "stroke": "#000",
    "stroke-linecap": "round",
    "stroke-width": 2
  });
  var tooltip = this.set(rect, textSet);

  rect.attr({
    "height" : tooltip.getBBox().height + 10,
    "width" : tooltip.getBBox().width + 10
  });
  
  tooltip.transform([
    't', 20, 20
  ]);
  
  return tooltip;
Valery Sizov's avatar
Valery Sizov committed
364
};
365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397

function textWrap(t, width) {
  var content = t.attr("text");
  var abc = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
  t.attr({
    "text" : abc
  });
  var letterWidth = t.getBBox().width / abc.length;
  
  t.attr({
    "text" : content
  });

  var words = content.split(" ");
  var x = 0, s = [];
  for ( var i = 0; i < words.length; i++) {

    var l = words[i].length;
    if (x + (l * letterWidth) > width) {
        s.push("\n");
        x = 0;
    }
    x += l * letterWidth;
    s.push(words[i] + " ");
  }
  t.attr({
    "text" : s.join("")
  });
  var b = t.getBBox()
    , h = Math.abs(b.y2) - Math.abs(b.y) + 1;
  t.attr({
    "y": b.y + h
  });
Sato Hiroyuki's avatar
Sato Hiroyuki committed
398
}