Skip to main content

Overview

The visualization system renders the git commit graph as an interactive SVG visualization. It uses Raphael.js for cross-browser SVG rendering and implements a sophisticated animation system for smooth transitions. Location: src/js/visuals/

Architecture

Component Hierarchy

Visualization (visualization.js)
  └─> GitVisuals (index.js)
      ├─> VisTree (tree.js)
      ├─> VisNode (visNode.js) - Individual commits
      ├─> VisBranch (visBranch.js) - Branch labels
      ├─> VisTag (visTag.js) - Tag labels  
      ├─> VisEdge (visEdge.js) - Commit parent arrows
      └─> Animation system (animation/)

Core Classes

Visualization

Main visualization controller that manages the canvas and coordinates rendering. Location: src/js/visuals/visualization.js

Constructor

class Visualization {
  constructor(options = {}) {
    this.options = options;
    this.containerElement = options.containerElement;
    this.el = options.el || document.body;
    this.$el = $(this.el);
    
    // Create Raphael canvas
    var container = options.containerElement || $('#canvasHolder')[0];
    new Raphael(container, 200, 200, function() {
      var paper = this;
      process.nextTick(function() {
        _this.paperInitialize(paper, options);
      });
    });
  }
}

Key Methods

paperInitialize(paper, options) Initializes all visualization components:
paperInitialize(paper, options) {
  this.treeString = options.treeString;
  this.paper = paper;
  
  // Create collections
  this.commitCollection = new CommitCollection();
  this.branchCollection = new BranchCollection();
  this.tagCollection = new TagCollection();
  
  // Create visual system
  this.gitVisuals = new GitVisuals({
    commitCollection: this.commitCollection,
    branchCollection: this.branchCollection,
    tagCollection: this.tagCollection,
    paper: this.paper,
    noClick: this.options.noClick,
    isGoalVis: this.options.isGoalVis,
    smallCanvas: this.options.smallCanvas,
    visualization: this
  });
  
  // Create git engine
  this.gitEngine = new GitEngine({
    collection: this.commitCollection,
    branches: this.branchCollection,
    tags: this.tagCollection,
    gitVisuals: this.gitVisuals,
    eventBaton: this.eventBaton
  });
  
  this.gitEngine.init();
  this.gitVisuals.assignGitEngine(this.gitEngine);
  
  // Setup responsive resize
  $(window).on('resize', () => this.myResize());
  
  this.gitVisuals.drawTreeFirstTime();
  if (this.treeString) {
    this.gitEngine.loadTreeFromString(this.treeString);
  }
  
  this.fadeTreeIn();
  this.customEvents.trigger('gitEngineReady');
}
myResize() Handles canvas resizing:
myResize() {
  if (!this.paper) return;
  
  var elSize = this.el.getBoundingClientRect();
  var width = elSize.width;
  var height = elSize.height;
  
  if (!this.containerElement) {
    var left = this.$el.offset().left;
    var top = this.$el.offset().top;
    
    $(this.paper.canvas).css({
      position: 'absolute',
      left: left + 'px',
      top: top + 'px'
    });
  }
  
  this.paper.setSize(width, height);
  this.gitVisuals.canvasResize(width, height);
}
makeOrigin(options) Creates a remote visualization:
makeOrigin(options) {
  this.originVis = new Visualization(Object.assign(
    {},
    this.options,
    {
      noKeyboardInput: true,
      noClick: true,
      treeString: options.treeString
    }
  ));
  
  // Sync z-index
  this.originVis.customEvents.on('paperReady', function() {
    var value = $(this.paper.canvas).css('z-index');
    this.originVis.setTreeIndex(value);
  }.bind(this));
  
  return this.originVis;
}

VisBase

Base class for all visual elements with common functionality. Location: src/js/visuals/visBase.js
class VisBase {
  constructor(options = {}) {
    this._events = {};
    this.attributes = Object.assign({}, options);
  }
  
  get(key) {
    return this.attributes[key];
  }
  
  set(key, value) {
    if (typeof key === 'object') {
      Object.assign(this.attributes, key);
    } else {
      this.attributes[key] = value;
    }
    return this;
  }
  
  animateAttrKeys(keys, attrObj, speed, easing) {
    keys = Object.assign({}, {
      include: ['circle', 'arrow', 'rect', 'path', 'text'],
      exclude: []
    }, keys || {});
    
    var attr = this.getAttributes();
    keys.include.forEach(function(key) {
      attr[key] = Object.assign({}, attr[key], attrObj);
    });
    
    this.animateToAttr(attr, speed, easing);
  }
}

VisNode

Represents a single commit in the visualization. Location: src/js/visuals/visNode.js

Structure

class VisNode extends VisBase {
  constructor(options) {
    var defaults = {
      depth: undefined,
      outgoingEdges: null,
      circle: null,        // Raphael circle element
      text: null,          // Raphael text element
      id: null,            // Commit ID (e.g., 'C1')
      pos: null,           // {x, y} position
      radius: null,
      commit: null,        // Reference to Commit model
      fill: GRAPHICS.defaultNodeFill,
      'stroke-width': GRAPHICS.defaultNodeStrokeWidth,
      stroke: GRAPHICS.defaultNodeStroke
    };
    super(Object.assign({}, defaults, options));
  }
}

Key Methods

genGraphics() Creates SVG elements:
genGraphics() {
  var paper = this.gitVisuals.paper;
  var pos = this.getScreenCoords();
  
  // Create circle
  var circle = paper.circle(pos.x, pos.y, this.getRadius())
    .attr(this.getAttributes().circle);
  
  // Create text label
  var text = paper.text(pos.x, pos.y, String(this.get('id')));
  text.attr({
    'font-size': this.getFontSize(this.get('id')),
    'font-weight': 'bold',
    'font-family': 'Menlo, Monaco, Consolas, "Droid Sans Mono", monospace',
    opacity: this.getOpacity()
  });
  
  this.set('circle', circle);
  this.set('text', text);
  this.attachClickHandlers();
}
getAttributes() Calculates visual attributes:
getAttributes() {
  var pos = this.getScreenCoords();
  var opacity = this.getOpacity();
  var dashArray = (this.getIsInOrigin()) ? GRAPHICS.originDash : '';
  
  return {
    circle: {
      cx: pos.x,
      cy: pos.y,
      opacity: opacity,
      r: this.getRadius(),
      fill: this.getFill(),
      'stroke-width': this.get('stroke-width'),
      'stroke-dasharray': dashArray,
      stroke: this.get('stroke')
    },
    text: {
      x: pos.x,
      y: pos.y,
      opacity: opacity
    }
  };
}
setBirth() / setBirthPosition() Positions new commits at parent location:
setBirthPosition() {
  var parentCoords = this.getParentScreenCoords();
  
  this.get('circle').attr({
    cx: parentCoords.x,
    cy: parentCoords.y,
    opacity: 0,
    r: 0
  });
  
  this.get('text').attr({
    x: parentCoords.x,
    y: parentCoords.y,
    opacity: 0
  });
}
getFill() Determines commit color based on branch:
getFill() {
  var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('commit'));
  
  if (stat == 'head') {
    return GRAPHICS.headRectFill;
  } else if (stat == 'tag' || stat == 'none') {
    return GRAPHICS.orphanNodeFill;
  }
  
  return this.gitVisuals.getBlendedHuesForCommit(this.get('commit'));
}

VisBranch

Renders branch labels with arrows pointing to commits. Location: src/js/visuals/visBranch.js

Structure

class VisBranch extends VisBase {
  constructor(options) {
    var defaults = {
      text: null,          // Branch name text
      rect: null,          // Background rectangle
      arrow: null,         // Arrow pointing to commit
      isHead: false,       // Is this HEAD?
      flip: 1,             // -1 or 1, controls side
      fill: GRAPHICS.rectFill,
      offsetX: GRAPHICS.nodeRadius * 4.75,
      offsetY: 0,
      arrowHeight: 14,
      arrowLength: 14
    };
    super(Object.assign({}, defaults, options));
  }
}

Color Assignment

Branches use an accessible color palette:
const BRANCH_COLOR_PALETTE = [
  '#0074D9', // Blue
  '#FF851B', // Orange  
  '#2ECC40', // Green
  '#FF4136', // Red
  '#B10DC9', // Purple
  '#39CCCC', // Teal
  '#8B4513', // Brown
  '#F012BE'  // Pink
];

const BRANCH_NAME_COLOR_MAP = {
  main: '#00FF7F',
  master: '#0074D9',
  bugFix: '#4682B4',
  develop: '#2ECC40',
  feature: '#F012BE',
  release: '#B10DC9',
  hotfix: '#39CCCC'
};

function getBranchColor(branchName) {
  if (BRANCH_NAME_COLOR_MAP[branchName]) {
    return BRANCH_NAME_COLOR_MAP[branchName];
  }
  if (assignedBranchColors[branchName]) {
    return assignedBranchColors[branchName];
  }
  const color = BRANCH_COLOR_PALETTE[branchColorIndex % BRANCH_COLOR_PALETTE.length];
  assignedBranchColors[branchName] = color;
  branchColorIndex++;
  return color;
}

Arrow Generation

getArrowPath() {
  var f = this.get('flip');
  var arrowTip = offset2d(this.getCommitPosition(),
    f * this.get('arrowOffsetFromCircleX'), 0);
  
  var arrowEdgeUp = offset2d(arrowTip, 
    f * this.get('arrowLength'), 
    -this.get('arrowHeight'));
  var arrowEdgeLow = offset2d(arrowTip, 
    f * this.get('arrowLength'), 
    this.get('arrowHeight'));
  
  var arrowInnerUp = offset2d(arrowEdgeUp,
    f * this.get('arrowInnerSkew'),
    this.get('arrowEdgeHeight'));
  var arrowInnerLow = offset2d(arrowEdgeLow,
    f * this.get('arrowInnerSkew'),
    -this.get('arrowEdgeHeight'));
  
  var tailLength = 49;
  var arrowStartUp = offset2d(arrowInnerUp, f * tailLength, 0);
  var arrowStartLow = offset2d(arrowInnerLow, f * tailLength, 0);
  
  var pathStr = 'M' + toStringCoords(arrowStartUp) + ' ';
  [arrowInnerUp, arrowEdgeUp, arrowTip, arrowEdgeLow, arrowInnerLow, arrowStartLow]
    .forEach(pos => pathStr += 'L' + toStringCoords(pos) + ' ');
  pathStr += 'z';
  
  return pathStr;
}

Branch Stacking

Multiple branches on same commit are stacked:
getBranchStackIndex() {
  if (this.get('isHead')) {
    return 0;
  }
  
  var myArray = this.getBranchStackArray();
  var index = -1;
  myArray.forEach(function(branch, i) {
    if (branch.obj == this.get('branch')) {
      index = i;
    }
  }, this);
  return index;
}

getTextPosition() {
  var pos = this.getCommitPosition();
  var myPos = this.getBranchStackIndex();
  
  return {
    x: pos.x + this.get('flip') * this.get('offsetX'),
    y: pos.y + myPos * GRAPHICS.multiBranchY + this.get('offsetY')
  };
}

VisEdge

Draws Bezier curves connecting commits to parents. Location: src/js/visuals/visEdge.js
genSmoothBezierPathString() {
  var tail = this.get('tail').getScreenCoords();
  var head = this.get('head').getScreenCoords();
  
  // Calculate control points for smooth curve
  var delta = {
    x: head.x - tail.x,
    y: head.y - tail.y
  };
  
  var c1 = {
    x: tail.x + delta.x * 0.25,
    y: tail.y + delta.y * 0.75
  };
  
  var c2 = {
    x: tail.x + delta.x * 0.75,
    y: tail.y + delta.y * 0.25
  };
  
  return 'M' + tail.x + ',' + tail.y +
         'C' + c1.x + ',' + c1.y + ' ' +
               c2.x + ',' + c2.y + ' ' +
               head.x + ',' + head.y;
}

Animation System

Location: src/js/visuals/animation/

Animation Classes

Animation Basic animation with closure and duration:
class Animation {
  constructor(options) {
    this.closure = options.closure;
    this.duration = options.duration || GRAPHICS.defaultAnimationTime;
  }
  
  run() {
    this.closure();
  }
}
PromiseAnimation Promise-based animation:
class PromiseAnimation {
  constructor(options) {
    this.animation = options.animation || options.closure;
    this.duration = options.duration || GRAPHICS.defaultAnimationTime;
    this.deferred = createDeferred();
  }
  
  play() {
    if (typeof this.animation === 'function') {
      this.animation();
    } else {
      this.animation.animation();
    }
    
    setTimeout(() => {
      this.deferred.resolve();
    }, this.duration);
  }
  
  getPromise() {
    return this.deferred.promise;
  }
}

AnimationFactory

Creates common animation patterns. Location: src/js/visuals/animation/animationFactory.js Commit Birth Animation
var makeCommitBirthAnimation = function(gitVisuals, visNode) {
  var time = GRAPHICS.defaultAnimationTime * 1.0;
  var bounceTime = time * 2;
  
  var animation = function() {
    gitVisuals.refreshTree(time);
    
    // Position at parent
    visNode.setBirth();
    visNode.parentInFront();
    gitVisuals.visBranchesFront();
    
    // Animate to final position with bounce
    visNode.animateUpdatedPosition(bounceTime, 'bounce');
    visNode.animateOutgoingEdges(time);
  };
  
  return {
    animation: animation,
    duration: Math.max(time, bounceTime)
  };
};

AnimationFactory.playCommitBirthPromiseAnimation = function(commit, gitVisuals) {
  var animation = this.genCommitBirthPromiseAnimation(commit, gitVisuals);
  animation.play();
  return animation.getPromise();
};
Highlight Animation
var makeHighlightAnimation = function(visNode, visBranch) {
  var fullTime = GRAPHICS.defaultAnimationTime * 0.66;
  var slowTime = fullTime * 2.0;
  
  return {
    animation: function() {
      visNode.highlightTo(visBranch, slowTime, 'easeInOut');
    },
    duration: slowTime * 1.5
  };
};

AnimationFactory.playHighlightPromiseAnimation = function(commit, destObj) {
  var animation = this.genHighlightPromiseAnimation(commit, destObj);
  animation.play();
  return animation.getPromise();
};
Refresh Animation
AnimationFactory.playRefreshAnimation = function(gitVisuals, speed) {
  var animation = new PromiseAnimation({
    duration: speed,
    closure: function() {
      gitVisuals.refreshTree(speed);
    }
  });
  animation.play();
  return animation.getPromise();
};

Animation Queue

Manages sequential animations:
class AnimationQueue {
  constructor(options) {
    this.animations = [];
    this.callback = options.callback;
  }
  
  add(animation) {
    this.animations.push(animation);
  }
  
  thenFinish(promise) {
    promise.then(() => {
      this.callback();
    });
  }
}

Coordinate System

Tree Layout

Commits are positioned using a depth-based layout:
// Position calculation
setDepthBasedOn(depthIncrement, offset) {
  var pos = this.get('pos');
  pos.y = this.get('depth') * depthIncrement + offset;
}

// Screen coordinate conversion
toScreenCoords(pos) {
  return {
    x: pos.x * this.scaleFactor + this.offsetX,
    y: pos.y * this.scaleFactor + this.offsetY
  };
}

Responsive Sizing

canvasResize(width, height) {
  this.canvasWidth = width;
  this.canvasHeight = height;
  
  // Recalculate scale factor
  this.scaleFactor = Math.min(
    width / this.treeWidth,
    height / this.treeHeight
  );
  
  // Refresh all positions
  this.refreshTree();
}

Performance Optimizations

Dirty Tracking

refreshTree(speed) {
  if (!this.dirty) return;
  
  this.calcTreeCoords();
  this.animateAllFromTreeCoords(speed);
  this.dirty = false;
}

Layer Management

visBranchesFront() {
  this.visBranchCollection.each(function(visBranch) {
    visBranch.nonTextToFront();
  });
  this.visBranchCollection.each(function(visBranch) {
    visBranch.textToFront();
  });
}

Build docs developers (and LLMs) love