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() {
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) {
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() {
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
}
};
}
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() {
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();
}
}
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();
};
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();
};
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();
});
}
Related Components
- GitEngine - Manages commit data
- Command Processing - Triggers visual updates
- Level System - Uses visualization for goals