Created
April 9, 2010 19:47
-
-
Save thejefflarson/361512 to your computer and use it in GitHub Desktop.
This file contains hidden or 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