Skip to content

Instantly share code, notes, and snippets.

@thejefflarson
Created April 9, 2010 19:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thejefflarson/361512 to your computer and use it in GitHub Desktop.
Save thejefflarson/361512 to your computer and use it in GitHub Desktop.
;(function (){
var Graph;
window.Graph = Graph = function(el, data, template, highlight){
this.el = $(el);
this.width = this.el.width();
this.height = this.el.height();
this.borderWidth = 5;
this.data = data;
this.matrix = new Graph.Matrix();
this.hovering = false;
this.template = template;
this.initialHighlightKey = highlight;
this.animator = new Graph.Animator(this);
this.init();
return this;
};
Graph.prototype = {
init: function(){
var c = $("<canvas width='1' height='1'></canvas>").appendTo("body");
$.support.hasCanvas = !!$(c).attr('getContext');
c.remove();
if (!$.support.hasCanvas){
window.G_vmlCanvasManager.init_(document);
}
var canvas = document.createElement('canvas');
canvas.width = this.width;
canvas.height = this.height;
if (!$.support.hasCanvas){
canvas = window.G_vmlCanvasManager.initElement(canvas);
}
this.canvas = $(canvas).appendTo(this.el);
this.context = this.canvas.get(0).getContext('2d');
this.root = new Graph.Layer("root", {strokeStyle:"white", activeStyle: "white"});
this.root.active = true;
this.tip = $('<div class="tip"></div>').appendTo(this.el);
this.tip.css({position:"absolute"});
this.root.bind(this);
this.reset();
this.build();
this.grid();
this.draw();
this.listeners();
},
// These are member variables we'll want to reset from time to time.
reset: function(){
this.currentLayer = null;
this.highLight = null;
this.matrix.matrix = {};
this.matrix.cache = {};
this.root.children = [];
},
listeners: function(){
var self = this;
if(!($.browser.msie && $.browser.version === "6.0")){
$(this.canvas).bind('mousemove', function(e){
if(!self.hovering){
self.hovering = true;
var bounds = e.currentTarget.getBoundingClientRect();
e.localX = e.clientX - bounds.left + self.borderWidth;
e.localY = e.clientY - bounds.top;
$(self.canvas).queue(function(next){
_.bind(self.hoverCheck, self)(e);
next();
});
}
});
}
$(this.el).hover(function(){
},
function(e){
self.tip.fadeOut(100);
if(self.currentLayer){
self.currentLayer.layer.active = false;
self.currentLayer = null;
}
self.draw();
});
// to allow for any graphclick functions to be defined
function triggerClick(){
var meta = self.lastMetaByHighlight();
$(self.el).trigger("graphclick", {key: self.highLight.layer.key, layer:self.highLight, meta:meta});
}
setTimeout(function(){
triggerClick();
}, 15, null);
$(this.el).click(function(){
self.activateLayer("highLight", "highlight", self.currentLayer);
triggerClick();
self.draw();
});
},
removeListeners: function(){
$(this.el).unbind('graphclick');
$(this.el).children('canvas').unbind('mousemove');
},
draw: function(){
this.context.clearRect(0, 0, this.width, this.height);
this.root.draw();
},
//Takes care of layer initialization and lookup table building
build: function(){
this.reset();
var self = this;
_.each(this.data, function(series){
var key = _.keys(series).shift(),
series = _.values(series).shift(),
layer = new Graph.Layer(key, {
strokeStyle : "rgba(150,150,150,0.8)",
activeStyle : "red",
highlightStyle: "blue"
}),
lastPoint = self.p2c(self.point(series[0][0], series[0][1]))
;
self.root.addChild(layer);
layer.addCall("beginPath");
layer.addCall("moveTo", lastPoint.x+0.5, lastPoint.y+0.5);
_.each(series, function(point){
pt = self.p2c(self.point(point[0], point[1]));
point[2].name = key;
layer.addCall("lineTo", pt.x+0.5, pt.y+0.5);
self.matrix.addValue(pt, {layer: layer, meta:point[2]});
lastPoint = pt;
});
layer.addCall("stroke");
if(key === self.initialHighlightKey){
self.highLight = {layer: layer};
self.highLight.layer.highlight = true;
}
});
},
grid: function(){
var divisions = 5,
delta_y = Math.floor((this.max().y - this.min().y)/divisions),
delta_x = Math.floor((this.max().x - this.min().x)/divisions),
i = 0
;
for(; i < divisions; i++){
var x_div = this.min().x + (delta_x * i),
y_div = this.min().y + (delta_y * i),
point = this.point(x_div, y_div),
computedPoint = this.p2c(point)
;
$('<span class="label x">'+ new Date(x_div).format("mmm yyyy") +"</span>").css(
{position:"absolute",
left: computedPoint.x,
top: this.height+this.borderWidth,
"min-width": "100px"}).appendTo(this.el);
$('<span class="label y">'+ y_div +"%</span>").css(
{position:"absolute",
left: this.width+this.borderWidth*2 ,
top: computedPoint.y - 6,
"min-width": "100px"}).appendTo(this.el);
this.plotGrid(computedPoint);
}
},
plotGrid: function(point){
this.root.addCall("moveTo", point.x+0.5, 0+0.5);
this.root.addCall("lineTo", point.x+0.5, this.height+0.5);
this.root.addCall("moveTo", 0+0.5, point.y+0.5);
this.root.addCall("lineTo", this.width+0.5, point.y+0.5);
this.root.addCall("stroke");
},
lastMetaByHighlight: function(){
return _.last(_.last(_.compact(_.pluck(this.data, this.highLight.layer.key))[0]));
},
activate: function(key){
var ret = this.root.getByKey(key);
if(ret){
this.activateLayer("highLight", "highlight", {layer: ret});
ret = {layer: ret, key: key};
ret.meta = this.lastMetaByHighlight();
return ret;
}
},
activateLayer: function(key, subkey, layer){
if(layer && layer !== this[key]){
if(this[key] !== null && this[key])
this[key].layer[subkey] = false;
this[key] = layer;
this[key].layer[subkey] = true;
this.root.children = _.without(this.root.children, layer.layer);
this.root.children.push(layer.layer);
this.draw();
}
},
hoverCheck: function(e){
var pt = this.point(e.localX, e.localY),
layer = this.matrix.nearest(pt),
self = this
;
this.activateLayer("currentLayer", "active", layer);
if(this.currentLayer !== null && this.currentLayer){
this.tip.html(this.template(this.currentLayer.meta));
$(this.el).children("canvas").queue(function(next){
self.tip.css({
top:e.localY + 30,
left:e.localX - self.tip.width()/2}).fadeIn(100);
next();
});
}
this.hovering = false;
},
max: function (){
function maxTest(max, memo){
max.x = max.x > memo.x ? max.x : memo.x;
max.y = max.y > memo.y ? max.y : memo.y;
return max;
}
if(!this.maxTuple){
this.maxTuple = this.reduce(maxTest, this.point(-Infinity,100)); // hacky hack hack
}
return this.maxTuple;
},
min: function (){
function minTest(min, memo){
min.x = min.x < memo.x ? min.x : memo.x;
min.y = min.y < memo.y ? min.y : memo.y;
return min;
}
if(!this.minTuple){
this.minTuple = this.reduce(minTest, this.point(Infinity,Infinity));
}
return this.minTuple;
},
reduce: function(fn, initial){
var self = this;
return _.reduce(this.data, initial, function(memo, series){
var val = _.reduce(_.values(series).shift(), memo, function(innerMemo, innerVal){
return fn(self.point(innerVal[0], innerVal[1]), innerMemo);
});
return fn(val, memo);
});
},
point: function(x,y){
return {"x": x, "y": y};
},
// Translation functions
p2c: function(pt){
pt.x = Math.floor((pt.x - this.min().x) * this.width /
(this.max().x - this.min().x));
pt.y = Math.floor(this.height - ((pt.y - this.min().y) * this.height /
(this.max().y - this.min().y)));
return pt;
}
};
Graph.Layer = function(key, styles){
styles = styles || {}
this.children = [];
this.key = key;
this.calls = [];
this.active = false;
this.highlight = false;
this.styles = styles;
};
Graph.Layer.prototype = {
bind: function(parent){
this.parent = parent;
this.borderWidth = parent.borderWidth;
this.width = parent.width;
this.height = parent.height;
this.context = parent.context;
var self = this;
_.each(this.children, function(child){
child.bind(self);
});
},
addChild: function(child){
child.bind(this);
this.children.push(child);
},
getByKey: function(key){
var ret = null
if(this.key === key)
return this;
_.each(this.children, function(child){
layer = child.getByKey(key);
if(layer){
ret = layer;
_.breakLoop();
}
});
return ret;
},
addCall: function(call){
var args = Array.prototype.slice.call(arguments);
this.calls.push(args);
},
draw: function(){
var self = this;
this.context.save();
this.context.strokeStyle = this.highlight ?
this.styles.highlightStyle : this.active ?
this.styles.activeStyle : this.styles.strokeStyle;
this.context.translate(this.borderWidth, this.borderWidth);
this.context.scale(1-(this.borderWidth*2) / (this.width),
1-(this.borderWidth*2) / (this.height));
this.context.lineWidth = this.highlight ? 1.5 : 1;
_.each(this.calls, function(call){
self.context[call[0]].apply(self.context, call.slice(1, call.length));
});
this.context.restore();
_.each(this.children, function(child){
child.draw();
});
}
};
Graph.Matrix = function(){
this.matrix = {};
this.cache = {};
};
Graph.Matrix.prototype = {
addValue: function(pt, layer){
if(!this.matrix.hasOwnProperty(pt.x)){
this.matrix[pt.x] = {};
}
this.matrix[pt.x][pt.y] = layer;
},
nearest: function(pt){
var keys = this.sortStringArray(_.keys(this.matrix));
var x = this.search(keys, pt.x, 0, keys.length-1);
keys = this.sortStringArray(_.keys(this.matrix[x]), x);
var y = this.search(keys, pt.y, 0, keys.length-1);
if(!!x && !!y)
return this.matrix[x][y];
},
sortStringArray: function(keys, cache_key){
cache_key = cache_key || "exes";
if(this.cache.hasOwnProperty(cache_key)){
return this.cache[cache_key];
}
sorted = _.map(keys, function(key){
return parseInt(key,10)
}).sort(function(a,b){
return a-b;
});
this.cache[cache_key] = sorted;
return this.cache[cache_key];
},
search: function(vals, key, low, high){
if( high < low ){
var delta_h = Math.abs(key - vals[high]),
delta_l = Math.abs(key - vals[low]);
index = delta_h < delta_l ? high : low;
var ret = vals[index];
return ret;
}
mid = low + Math.floor((high-low)/2);
if(vals[mid] > key){
return this.search(vals, key, low, mid - 1);
} else if (vals[mid] < key) {
return this.search(vals, key, mid + 1, high);
} else {
return vals[mid];
}
},
addVector: function(fromPt, toPt, layer){
this.addValue(fromPt, layer);
this.addValue(toPt, layer);
},
matrix: function(){
return this.matrix;
}
};
Graph.Animator = function(graph){
this.graph = graph;
this.frameLength = 20.0; // ms / fps
this.duration = 60.0;
}
Graph.Animator.prototype = {
animate: function(data){
this.cachedData = graph.data;
this.newData = data;
this.graph.initialHighlightKey = this.graph.highLight.layer.key;
var self = this,
counter = 0
;
this.graph.removeListeners();
this.graph.reset();
if(!$.support.hasCanvas){
self.graph.data = this.newData;
self.graph.build();
self.graph.draw();
this.graph.listeners();
return;
}
while(counter <= this.duration){
this.add(function(next){
var step = self.frameLength / self.duration;
var delta = _.map(self.graph.data, function(series, i){
var key = _.keys(series).shift();
values = _.values(series).shift();
;
values = _.map(values, function(point, j){
var current = point,
source = self.cachedData[i][key][j],
dest = self.newData[i][key][j],
delta_x = point[0] + (dest[0]-point[0])*(step),
delta_y = point[1] + (dest[1]-point[1])*(step)
;
return [delta_x, delta_y, point[2]];
});
ret = {};
ret[key] = values;
return ret;
});
self.graph.data = delta;
self.graph.build();
self.graph.draw();
next();
});
counter = counter + this.frameLength;
}
this.add(function(next){
self.graph.data = data;
self.graph.build();
self.graph.listeners();
self.graph.draw();
next();
});
},
add: function(fn){
$(this.graph.el).children("canvas").queue(fn).delay(this.frameLength);
}
}
}());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment