Created
April 9, 2010 19:47
-
-
Save thejefflarson/361512 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;(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