/* Plotr V0.1.4 ============ This is a uncompressed version of Plotr.{Base,Chart,Canvas,LineChart,BarChart}, packed by Dean Edwards's Packer. For license/info/documentation: http://www.solutoire.com/plotr/ Credits ------- Plotr is partially based on PlotKit (BSD license) by Alastair Tse . Copyright --------- Copyright 2007 (c) Bas Wenneker For use under the BSD license. */ try { if (typeof(Prototype) == 'undefined') throw ''; } catch(e) { throw 'Plotr depends on the Prototype framework (version 1.5.0).'; }; if (typeof(Plotr) == 'undefined') { Plotr = {}; }; Plotr.name = 'Plotr'; Plotr.version = '0.1.4'; Plotr.author = 'Bas Wenneker'; if (typeof(Plotr.Base) == 'undefined') { Plotr.Base = {}; }; /** * Plotr.Base.items puts all (non Function) items of lst into * an Array and returns the Array. * * @alias items * @param {Object} lst * @return {Array} result - Array that contains all nonFunction items of lst. */ Plotr.Base.items = function(lst) { var result = new Array(); for(var item in lst){ if (typeof(lst[item]) == 'function') continue; result.push(lst[item]); } return result; }; /** * Check if obj is null or undefined. * * @alias isNil * @param {Object} obj * @return {Boolean} true if null or undefined. */ Plotr.Base.isNil = function(obj) { return (obj == null || typeof(obj) == 'undefined'); }; /** * Check if canvas simulation by ExCanvas is supported by the browser. * * @alias excanvasSupported * @return {Boolean} true if userAgent is IE */ Plotr.Base.excanvasSupported = function() { if (/MSIE/.test(navigator.userAgent) && !window.opera) { return true; } return false; }; /** * Check whether or not canvas is supported by the browser. * * @alias isSupported * @param {String} canvasName - ID of the canvas element. * @return {Boolean} true if browser has canvas support supported. */ Plotr.Base.isSupported = function(canvasName) { var canvas = null; try { if (Plotr.Base.isNil(canvasName)) canvas = document.createElement('canvas'); else canvas = $(canvasName); var context = canvas.getContext('2d'); } catch(e) { var ie = navigator.appVersion.match(/MSIE (\d\.\d)/); var opera = (navigator.userAgent.toLowerCase().indexOf('opera') != -1); if ((!ie) || (ie[1] < 6) || (opera)) return false; return true; } return true; }; /** * This function checks lst for the element with the largest length and * then returns an array with element 0 .. length. * * @alias uniqueIndices * @param {Array} lst * @return {Array} result - Returns an array with unique numbers. */ Plotr.Base.uniqueIndices = function(lst) { var result = new Array(); lst.max(function(item){ return item.length; }).times(function(i){ result.push(i); }); return result; }; Plotr.Base.sum = function(lst){ lst = lst.flatten(); var result = 0; for(var i = lst.length-1; i >= 0; i--) result += lst[i]; return result; }; /** * Convert an string with an hexadecimal color code {'#ffffff','ffffff'} * to an RGB object {r: int, g: int, b: int}. * * @alias hexToRGB * @param {String} hex - String with hexadecimal color code like '#ffffff' or 'ffffff'. * @return {Object} rgbObj - Returns an object {r: int, g: int, b: int}. */ Plotr.Base.hexToRGB = function(hex) { hex = parseInt(((hex.indexOf('#') > -1) ? hex.substring(1) : hex), 16); return {r: hex >> 16, g: (hex & 0x00FF00) >> 8, b: (hex & 0x0000FF)}; }; /** * Returns a String representation of rgbObj. * * @alias toRGBString * @param {Object} rgbObj - Object with RGB values {r: int, g: int, b: int}. * @return {String} rgb - Returns a String representation of the rgbObj. */ Plotr.Base.toRGBString = function(rgbObj) { return '#' + [rgbObj.r, rgbObj.g, rgbObj.b].invoke('toColorPart').join(''); }; /** * Convert an string with an hexadecimal color code {'#ffffff','ffffff'} * to an HSB object {h: int, s: int, b: int}. * * @alias hexToHSB * @param {String} hex - String with hexadecimal color code like '#ffffff' or 'ffffff'. * @return {Object} rgbObj - Returns an object {r: int, g: int, b: int}. */ Plotr.Base.hexToHSB = function(hex) { return Plotr.Base.rgbToHSB(Plotr.Base.hexToRGB(hex)); }; /** * Modification of Promeths' script. Converts a rgbObj {r: int, g: int, b: int} * to a hsbObj {h: int, s: int, b: int}. * * @alias rgbToHSB * @param {Object} rgbObj - Object with RGB values {r: int, g: int, b: int}. * @return {Object} hsbObj - Returns an object with HSB values {h: int, s: int, b: int}. * @author Prometh - http://proto.layer51.com/d.aspx?f=1136 */ Plotr.Base.rgbToHSB = function(rgbObj) { var r = rgbObj.r; var g = rgbObj.g; var b = rgbObj.b; var hsb = {}; hsb.b = Math.max(Math.max(r,g),b); hsb.s = (hsb.b <= 0) ? 0 : Math.round(100*(hsb.b - Math.min(Math.min(r,g),b))/hsb.b); hsb.b = Math.round((hsb.b /255)*100); if((r==g) && (g==b)) hsb.h = 0; else if(r>=g && g>=b) hsb.h = 60*(g-b)/(r-b); else if(g>=r && r>=b) hsb.h = 60 + 60*(g-r)/(g-b); else if(g>=b && b>=r) hsb.h = 120 + 60*(b-r)/(g-r); else if(b>=g && g>=r) hsb.h = 180 + 60*(b-g)/(b-r); else if(b>=r && r>=g) hsb.h = 240 + 60*(r-g)/(b-g); else if(r>=b && b>=g) hsb.h = 300 + 60*(r-b)/(r-g); else hsb.h = 0; hsb.h = Math.round(hsb.h); return hsb; }; /** * Modification of Promeths' script. Converts a hsbObj {h: int, s: int, b: int} * to a rgbObj {r: int, g: int, b: int}. * * @alias hsbToRGB * @param {Object} hsbObj - Object with HSB value {h: int, s: int, b: int}. * @return {Object} rgbObj - Returns an Object with RGB values {r: int, g: int, b: int}. * @author Prometh - http://proto.layer51.com/d.aspx?f=1135 */ Plotr.Base.hsbToRGB = function(hsbObj) { var r,g,b; var h = Math.round(hsbObj.h); var s = Math.round(hsbObj.s*255/100); var v = Math.round(hsbObj.b*255/100); if(s == 0) { r = g = b = v; } else { var t1 = v; var t2 = (255-s)*v/255; var t3 = (t1-t2)*(h%60)/60; if(h==360) h = 0; if(h<60) {r=t1; b=t2; g=t2+t3} else if(h<120) {g=t1; b=t2; r=t1-t3} else if(h<180) {g=t1; r=t2; b=t2+t3} else if(h<240) {b=t1; r=t2; g=t1-t3} else if(h<300) {b=t1; g=t2; r=t2+t3} else if(h<360) {r=t1; g=t2; b=t1-t3} else {r=0; g=0; b=0} } return {r:Math.round(r), g:Math.round(g), b:Math.round(b)}; }; /** * Plotr.Base.levelHSB takes an hsbObj alters the brightness of it. * The brightness will be the max(initial brightness + level, 100). * * @alias levelHSB * @param {Object} hsbObj - Object with HSB values {h: int, s: int, b: int}. * @param {Object} level - Increase Brightness with level. * @return {String} rgbString - Returns a RGB string representation of the leveled hsbObj. */ Plotr.Base.levelHSB = function(hsbObj, level) { var hsb = { h: hsbObj.h, s: hsbObj.s, b: Math.min(hsbObj.b + level, 100) }; return Plotr.Base.toRGBString(Plotr.Base.hsbToRGB(hsb)); }; /** * Plotr.Base.generateColorscheme returns an Array with string representations of * colors. The colors start from 'hex' and every color after hex has the same Hue * and Saturation but a leveled Brightness. So colors go from dark to light. * If reverse is set to true, the colors go from light to dark. * * @alias generateColorscheme * @param {String} hex - String with hexadecimal color code like '#ffffff' or 'ffffff'. * @param {Integer} size - Size of the colorScheme. * @param {Boolean} reverse - True if you want the colorScheme to be reversed. * @return {Array} result - Returns a colorScheme Array of length 'size'. */ Plotr.Base.generateColorscheme = function(hex, size, reverse) { var hsb = Plotr.Base.hexToHSB(hex); var result = new Array(); var level = (100 - hsb.b)/size; size.times(function(index){ result.push(Plotr.Base.levelHSB(hsb, level*index)); }); return (reverse) ? result.reverse() : result; }; /** * Returns the default (green) colorScheme. * * @alias defaultScheme * @param {Integer} size - Size of the colorScheme to return. * @return {Array} colorScheme - Returns an Array of colors of length 'size'. */ Plotr.Base.defaultScheme = function(size) { return Plotr.Base.generateColorscheme('#3c581a',size) }; /** * Function returns a colorScheme based on 'color' of length 'size'. * * @alias getColorscheme * @see Plotr.Base.colorSchemes * @param {String} color * @param {Object} size - Size the colorScheme that's returned should have. * @return {Array} colorScheme - Returns an Array of colors of length 'size'. */ Plotr.Base.getColorscheme = function(color, size) { var scheme = Plotr.Base.colorSchemes[color]; if(Plotr.Base.isNil(scheme)){ return Plotr.Base.generateColorscheme(color,(size) ? size : 3); } return Plotr.Base.generateColorscheme(scheme,(size) ? size : 3); }; /** * Storage of colors for predefined colorSchemes. * * @alias colorSchemes */ Plotr.Base.colorSchemes = { red: '#6d1d1d', green: '#3c581a', blue: '#224565', grey: '#444444', black: '#000000' }; /** * Plotr.Chart * * @alias Plotr.Chart * @namespace Plotr */ Plotr.Chart = { /** * The constructor of Plotr.Chart. * * @alias initialize * @see Plotr.Canvas.setOptions * @param {String} type - Choose from {'bar'}. * @param {Object} options - Object with options. */ initialize: function(element, options) { this.setOptions(options); this.sets = 0; this.dataStores = new Array(); if (!Plotr.Base.isNil(this.options.xAxis)) { this.minxval = this.options.xAxis[0]; this.maxxval = this.options.xAxis[1]; this.xscale = this.maxxval - this.minxval; } else { this.minxval = 0; this.maxxval = this.xscale = null; } if (!Plotr.Base.isNil(this.options.yAxis)) { this.minyval = this.options.yAxis[0]; this.maxyval = this.options.yAxis[1]; this.yscale = this.maxyval - this.minyval; } else { this.minyval = 0; this.maxyval = this.yscale = null; } this.xticks = new Array(); this.yticks = new Array(); this.minxdelta = 0; this.xrange = 1; this.yrange = 1; this._initCanvas(element); }, /** * Function adds the array to the dataStores object. Argument must be in * the form of: {['': [[0,1],[1,2]...], ..}. The function also * keeps track of how many sets are added. * * @alias addDataset * @param {Object} arguments - Object with data */ addDataset: function(store) { for(var name in store) { this.dataStores[name] = store[name]; this.sets++; } }, /** * This function makes it easy to parse a table and show it's * data in a chart. The upper left corner has coordinates (x=0,y=0). * * @alias addTable * @param {String|Element} table - table id or element; * @param {Integer} x - xcoordinate to start with data parsing * @param {Integer} y - y coordinate to start with data parsing * @param {Integer} xticks - row number of row with labels for xticks */ addTable: function(table, x, y, xticks){ table = $(table); if(Plotr.Base.isNil(x)) x = 0; if(Plotr.Base.isNil(y)) y = 1; if(Plotr.Base.isNil(xticks)) xticks = -1; var tr = table.tBodies[0].rows; var store = {}; var labels = new Array(); for(var i = y, ln = tr.length; i < ln; i++){ var j = 0; store['row_'+i] = $A(tr[i].cells).reject(function(cell,index){ return index < x; }).collect(function(cell){ return [j++, parseFloat(cell.innerHTML)]; }); } if(xticks >= 0){ var tickIndex = 0; this.options.xTicks = $A(tr[xticks].cells).reject(function(cell,index){ return index < x; }).collect(function(cell){ return {v: tickIndex++, label: cell.innerHTML}; }); } this.addDataset(store); }, /** * This function does all the math. It'll process all the data that has to do * with canvas measures. * * @alias _eval * @param {Object} options - (optional) evaluate the chart with the given options. */ _eval: function(options) { if(!Plotr.Base.isNil(options)) { Object.extend(options,{}); this.setOptions(options); } this.stores = Plotr.Base.items(this.dataStores); this._evalXY(); this.setColorscheme(); }, /** * Processes measures of the bars(/lines/pies). * * @alias _evalXY */ _evalXY: function() { var xdata = this.stores.collect(function(item) {return item.pluck(0)}).flatten(); if (Plotr.Base.isNil(this.options.xAxis)) { this.minxval = (this.options.xOriginIsZero) ? 0 : parseFloat(xdata.min()); this.maxxval = parseFloat(xdata.max()); } else { this.minxval = this.options.xAxis[0]; this.maxxval = this.options.xAxis[1]; this.xscale = this.maxxval - this.minxval; } this.xrange = this.maxxval - this.minxval; this.xscale = (this.xrange == 0) ? 1.0 : 1/this.xrange; var ydata = this.stores.collect(function(item) {return item.pluck(1)}).flatten(); if (Plotr.Base.isNil(this.options.yAxis)) { this.minyval = (this.options.yOriginIsZero) ? 0 : parseFloat(ydata.min()); this.maxyval = parseFloat(ydata.max()); } else { this.minyval = this.options.yAxis[0]; this.maxyval = this.options.yAxis[1]; this.yscale = this.maxyval - this.minyval; } this.yrange = this.maxyval - this.minyval; this.yscale = (this.yrange == 0) ? 1.0 : 1/this.yrange; }, /** * Evaluates ticks for X and Y axis. * * @alias _evalLineTicks */ _evalLineTicks: function() { this._evalLineTicksForXAxis(); this._evalLineTicksForYAxis(); }, /** * Evaluates ticks for X axis. * * @alias _evalLineTicksForXAxis */ _evalLineTicksForXAxis: function() { if (this.options.xTicks) { this.xticks = this.options.xTicks.collect(function (tick) { var label = tick.label; if (Plotr.Base.isNil(label)) label = tick.v.toString(); var pos = this.xscale * (tick.v - this.minxval); if ((pos >= 0.0) && (pos <= 1.0)) { return [pos, label]; } }.bind(this)); } else if (this.options.xNumberOfTicks) { var uniqx = Plotr.Base.uniqueIndices(this.stores); var roughSeparation = this.xrange / this.options.xNumberOfTicks; var tickCount = 0; this.xticks = new Array(); for (var i = 0; i <= uniqx.length; i++) { if ((uniqx[i] - this.minxval) >= (tickCount * roughSeparation)) { var pos = this.xscale * (uniqx[i] - this.minxval); if ((pos > 1.0) || (pos < 0.0)) continue; this.xticks.push([pos, uniqx[i]]); tickCount++; } if (tickCount > this.options.xNumberOfTicks) break; } } }, /** * Evaluates ticks for Y axis. * * @alias _evalLineTicksForYAxis */ _evalLineTicksForYAxis: function() { if (this.options.yTicks) { this.yticks = this.options.yTicks.collect(function (tick) { var label = tick.label; if (Plotr.Base.isNil(label)) label = tick.v.toString(); var pos = 1.0 - (this.yscale * (tick.v - this.minyval)); if ((pos >= 0.0) && (pos <= 1.0)) { return [pos, label]; } }.bind(this)); }else if (this.options.yNumberOfTicks) { this.yticks = new Array(); var prec = this.options.yTickPrecision; var num = this.yrange/this.options.yNumberOfTicks; var roughSeparation = num.toFixed(this.options.yTickPrecision); for (var i = 0; i <= this.options.yNumberOfTicks; i++) { var yval = this.minyval + (i * roughSeparation); var pos = 1.0 - ((yval - this.minyval) * this.yscale); if ((pos > 1.0) || (pos < 0.0)) continue; this.yticks.push([pos, yval.toFixed(prec)]); } } } }; /** * Plotr.Canvas * * @alias Plotr.Canvas * @namespace Plotr */ Plotr.Canvas = { /** * Sets options of this chart. Current options are: * - sweetRender: using more advanced renderering techniques the chart will look much sweeter. {Boolean} * - drawBackground: whether a background should be drawn. {Boolean} * - backgroundLineColor: color of backgroundlines. {String} * - backgroundColor: background color. {String} * - padding: padding. {Object} * - colorScheme: Array of colors used for chart. {Array} * - strokeColor: color of a stroke. {String} * - strokeWidth: width of a stroke. {Number} * - shouldFill: whether bars/lines/pies should be filled. {Boolean} * - shouldStroke: whether strokes should be drawn. {Boolean} * - drawXAxis: whether the X axis should be drawn. {Boolean} * - drawYAxis: whether the Y axis should be drawn. {Boolean} * - axisTickSize: size of a tick in pixels. {Number} * - axisLineColor: color of axis lines. {String} * - axisLineWidth: line width of axis. {Number} * - axisLabelColor: axis label color. {String} * - axisLabelFont: font familily used for labels. {String} * - axisLabelFontSize: font size used for labels. {String} * - axisLabelWidth: axis label width. {Number} * - barWidthFillFraction: sets the bar width (>1 will cause bars to overlap eachother). {Integer} * - barOrientation: whether bars are horizontal. {'horizontal','vertical'} * - xOriginIsZero: whether or not the origin of the X axis starts at zero. {Boolean} * - yOriginIsZero: whether or not the origin of the Y axis starts at zero. {Boolean} * - xAxis: values of xAxis {[xmin,xmax]} * - yAxis: values of yAxis {[ymin,ymax]} * - xTicks: labels for the X axis. {[{label: "somelabel", v: value}, ..]} (label = optional) * - yTicks: labels for the Y axis. {[{label: "somelabel", v: value}, ..]} (label = optional) * - xNumberOfTicks: number of ticks on X axis when xTicks is null. {Integer} * - yNumberOfTicks: number of ticks on Y axis when yTicks is null. {Integer} * - xTickPrecision: decimals for the labels on the X axis. {Integer} * - yTickPrecision: decimals for the labels on the Y axis. {Integer} * * @alias Chart.setOptions * @param {Object} options - Object with options. */ setOptions: function(options) { this.options = Object.extend({ sweetRender: true, drawBackground: true, backgroundLineColor:'#ffffff', backgroundColor: '#f5f5f5', padding: {left: 30, right: 30, top: 5, bottom: 10}, colorScheme: Plotr.Base.defaultScheme(Math.max(this.sets,3)), strokeColor: '#ffffff', strokeWidth: 0.5, shouldFill: true, shouldStroke: true, drawXAxis: true, drawYAxis: true, axisTickSize: 3, axisLineColor: '#000000', axisLineWidth: 0.5, axisLabelColor: '#666666', axisLabelFont: 'Arial', axisLabelFontSize: 9, axisLabelWidth: 50, barWidthFillFraction: 0.75, barOrientation: 'vertical', xOriginIsZero: true, yOriginIsZero: true, xAxis: null, yAxis: null, xTicks: null, yTicks: null, xNumberOfTicks: 10, yNumberOfTicks: 10, xTickPrecision: 1, yTickPrecision: 1, pieRadius: 0.4 }, options || {}); }, /** * Resets options and datasets of this chart. * * @alias reset */ reset: function() { this.setOptions(); this.dataStores = new Array(); if(!Plotr.Base.isNil(this.renderDelay)){ this.renderDelay.stop(); this.renderDelay = null; } }, /** * The constructor of Plotr.Canvas. * * @alias initialize * @see setOptions * @param {String} element - Canvas element ID. * @param {Plotr.Chart} chart - Chart object to render. * @param {Object} options - Options. */ _initCanvas: function(element) { this.canvasNode = $(element); this.containerNode = this.canvasNode.parentNode; Element.setStyle(this.containerNode,{position: 'relative',width: this.canvasNode.width + 'px'}); this.isIE = Plotr.Base.excanvasSupported(); if (this.isIE && !Plotr.Base.isNil(G_vmlCanvasManager)) { this.IEDelay = 0.5; this.maxTries = 10; this.renderDelay = null; this.renderStack = new Array(); this.canvasNode = G_vmlCanvasManager.initElement(this.canvasNode); } if(Plotr.Base.isNil(this.canvasNode)) throw 'Plotr.Canvas(): Could\'nt find canvas.'; if(Plotr.Base.isNil(this.containerNode) || this.containerNode.nodeName.toLowerCase() != 'div') throw 'Plotr.Canvas(): Canvas element is not enclosed by a
element.'; if (!this.isIE && !(Plotr.Base.isSupported(element))) throw "Plotr.Canvas(): Canvas is not supported."; this.xlabels = new Array(); this.ylabels = new Array(); this.area = { x: this.options.padding.left, y: this.options.padding.top, w: this.canvasNode.width - this.options.padding.left - this.options.padding.right, h: this.canvasNode.height - this.options.padding.top - this.options.padding.bottom }; }, /** * This function renders the background in the canvas element. * * @alias render * @param {String} [element] - (optional) ID of the canvas element to render in. */ _render: function(element) { if(!Plotr.Base.isNil(element)) this._initCanvas(element); if (this.options.drawBackground) { this._renderBackground(); } }, _ieWaitForVML: function(element, options){ var isNil = Plotr.Base.isNil; if(!isNil(element)) { this.renderStack[element] = options; } try{ if(!isNil(this.canvasNode)) { this.canvasNode.getContext("2d"); } else { $(element).getContext("2d"); } } catch(e) { if(isNil(this.renderDelay)){ this.renderDelay = new PeriodicalExecuter(function(pe){ if(!isNil(this.canvasNode)) { this.render(this.canvasNode,options); } else { this.render(element,options); } }.bind(this), this.IEDelay); }else if(this.maxTries-- <= 0) { this.renderDelay.stop(); } return true; } if(!isNil(this.renderDelay)){ this.renderDelay.stop(); delete this.renderStack[element || this.canvasNode]; } return false; }, /** * Sets the colorScheme used for the chart. * * @alias setColorScheme */ setColorscheme: function() { var scheme = this.options.colorScheme; if(scheme instanceof Array) { return; } else if(typeof(scheme) == 'string') { if(this.type == 'pie'){ this.options.colorScheme = Plotr.Base.getColorscheme(scheme, Math.max(this.stores[0].length,3)); }else{ this.options.colorScheme = Plotr.Base.getColorscheme(scheme, Math.max(this.sets,3)); } } else { throw 'Plotr.Canvas.setColorscheme(): colorScheme is invalid!'; } }, /** * Renders the background of the chart. * * @alias _renderBackground */ _renderBackground: function() { var cx = this.canvasNode.getContext('2d'); cx.save(); cx.fillStyle = this.options.backgroundColor; if(this.options.sweetRender) { cx.fillRect(this.area.x, this.area.y, this.area.w, this.area.h); cx.strokeStyle = this.options.backgroundLineColor; cx.lineWidth = 1.0; var ticks = this.yticks; var horiz = false; if (this.type == 'bar' && this.options.barOrientation == 'horizontal') { ticks = this.xticks; horiz = true; } var drawBackgroundLines = function(tick) { var x1 = x2 = y1 = y2 = 0; if(horiz) { x1 = x2 = tick[0] * this.area.w + this.area.x; y1 = this.area.y; y2 = y1 + this.area.h; }else{ x1 = this.area.x; y1 = tick[0] * this.area.h + this.area.y; x2 = x1 + this.area.w; y2 = y1; } cx.beginPath(); cx.moveTo(x1, y1); cx.lineTo(x2, y2); cx.closePath(); cx.stroke(); }.bind(this); ticks.each(drawBackgroundLines); } else { cx.fillRect(0, 0, this.canvasNode.width, this.canvasNode.height); } cx.restore(); }, /** * Renders the axis for line charts. * * @alias _renderLineAxis */ _renderLineAxis: function() { this._renderAxis(); }, /** * Renders axis. * * @alias _renderAxis */ _renderAxis: function() { if (!this.options.drawXAxis && !this.options.drawYAxis) return; var cx = this.canvasNode.getContext('2d'); var labelStyle = { position: 'absolute', fontSize: this.options.axisLabelFontSize + 'px', fontFamily: this.options.axisLabelFont, zIndex: 10, color: this.options.axisLabelColor, width: this.options.axisLabelWidth + 'px', overflow: 'hidden' }; cx.save(); cx.strokeStyle = this.options.axisLineColor; cx.lineWidth = this.options.axisLineWidth; if (this.options.drawYAxis) { if (this.yticks) { var collectYLabels = function(tick) { if(typeof(tick) == 'function') return; var x = this.area.x; var y = this.area.y + tick[0] * this.area.h; cx.beginPath(); cx.moveTo(x, y); cx.lineTo(x - this.options.axisTickSize, y); cx.closePath(); cx.stroke(); var label = document.createElement('div'); label.appendChild(document.createTextNode(tick[1])); Element.setStyle(label, Object.extend(labelStyle,{ top: (y - this.options.axisLabelFontSize) + 'px', left: (x - this.options.padding.left - this.options.axisTickSize) + 'px', width: (this.options.padding.left - this.options.axisTickSize * 2) + 'px', textAlign: 'right' })); this.containerNode.appendChild(label); return label; }.bind(this); this.ylabels = this.yticks.collect(collectYLabels); } cx.beginPath(); cx.moveTo(this.area.x, this.area.y); cx.lineTo(this.area.x, this.area.y + this.area.h); cx.closePath(); cx.stroke(); } if(this.options.drawXAxis) { if(this.xticks) { var collectXLabels = function(tick) { if (typeof(tick) == 'function') return; var x = this.area.x + tick[0] * this.area.w; var y = this.area.y + this.area.h; cx.beginPath(); cx.moveTo(x, y); cx.lineTo(x, y + this.options.axisTickSize); cx.closePath(); cx.stroke(); var label = document.createElement('div'); label.appendChild(document.createTextNode(tick[1])); Element.setStyle(label, Object.extend(labelStyle,{ top: (y + this.options.axisTickSize) + 'px', left: (x - this.options.axisLabelWidth/2) + 'px', width: this.options.axisLabelWidth + 'px', textAlign: 'center' })); this.containerNode.appendChild(label); return label; }.bind(this); this.xlabels = this.xticks.collect(collectXLabels); } cx.beginPath(); cx.moveTo(this.area.x, this.area.y + this.area.h); cx.lineTo(this.area.x + this.area.w, this.area.y + this.area.h); cx.closePath(); cx.stroke(); } cx.restore(); } }; Plotr.BarChart = Class.create(); Object.extend(Plotr.BarChart.prototype, Plotr.Canvas); Object.extend(Plotr.BarChart.prototype, Plotr.Chart); Object.extend(Plotr.BarChart.prototype,{ /** * Type of chart we're dealing with. */ type: 'bar', /** * Renders the chart with the specified options. The optional parameters * can be used to render a barchart in a different canvas element with new options. * * @alias render * @param {String} [element] - (optional) ID of a canvas element. * @param {Object} [options] - (optional) Options for rendering. */ render: function(element, options) { var isNil = Plotr.Base.isNil; if(this.isIE && this._ieWaitForVML(element,options)){ return; } this._evaluate(options); this._render(element); this._renderBarChart(); this._renderBarAxis(); if(this.isIE) { for(var el in this.renderStack){ if(typeof(this.renderStack[el]) == 'function') break; this.render(el,this.renderStack[el]); break; } } }, /** * This function does all the math. This function evaluates all the data needed * to plot the chart. * * @alias _evaluate * @param {Object} [options] - (optional) Evaluate the chart with the given options. */ _evaluate: function(options) { this._eval(options); if(this.options.barOrientation == 'vertical') this._evalBarChart(); else this._evalHorizBarChart(); this._evalBarTicks(); }, /** * Evaluates measures for vertical bars. * * @alias _evalBarChart */ _evalBarChart: function() { var uniqx = Plotr.Base.uniqueIndices(this.stores); var xdelta = 10000000; for (var i = 1; i < uniqx.length; i++) { xdelta = Math.min(Math.abs(uniqx[i] - uniqx[i-1]), xdelta); } var barWidth = 0; var barWidthForSet = 0; var barMargin = 0; if (uniqx.length == 1) { xdelta = 1.0; this.xscale = 1.0; this.minxval = uniqx[0]; barWidth = 1.0 * this.options.barWidthFillFraction; barWidthForSet = barWidth / this.stores.length; barMargin = (1.0 - this.options.barWidthFillFraction)/2; } else { this.xscale = (this.xrange == 1) ? 0.5 : (this.xrange == 2) ? 1/3.0 : (1.0 - 1/this.xrange)/this.xrange; barWidth = xdelta * this.xscale * this.options.barWidthFillFraction; barWidthForSet = barWidth / this.stores.length; barMargin = xdelta * this.xscale * (1.0 - this.options.barWidthFillFraction)/2; } this.minxdelta = xdelta; this.bars = new Array(); var i = 0; for (var name in this.dataStores) { var store = this.dataStores[name]; if(typeof(store) == 'function') continue; for (var j = 0; j < store.length; j++) { var item = store[j]; var rect = { x: ((parseFloat(item[0]) - this.minxval) * this.xscale) + (i * barWidthForSet) + barMargin, y: 1.0 - ((parseFloat(item[1]) - this.minyval) * this.yscale), w: barWidthForSet, h: ((parseFloat(item[1]) - this.minyval) * this.yscale), xval: parseFloat(item[0]), yval: parseFloat(item[1]), name: name }; if ((rect.x >= 0.0) && (rect.x <= 1.0) && (rect.y >= 0.0) && (rect.y <= 1.0)) { this.bars.push(rect); } } i++; } }, /** * Evaluates measures for vertical bars. * * @alias _evalHorizBarChart */ _evalHorizBarChart: function() { var uniqx = Plotr.Base.uniqueIndices(this.stores); var xdelta = 10000000; for (var i = 1; i < uniqx.length; i++) { xdelta = Math.min(Math.abs(uniqx[i] - uniqx[i-1]), xdelta); } var barWidth = 0; var barWidthForSet = 0; var barMargin = 0; if (uniqx.length == 1) { xdelta = 1.0; this.xscale = 1.0; this.minxval = uniqx[0]; barWidth = 1.0 * this.options.barWidthFillFraction; barWidthForSet = barWidth / this.stores.length; barMargin = (1.0 - this.options.barWidthFillFraction)/2; } else { this.xscale = (1.0 - xdelta/this.xrange)/this.xrange; barWidth = xdelta * this.xscale * this.options.barWidthFillFraction; barWidthForSet = barWidth / this.stores.length; barMargin = xdelta * this.xscale * (1.0 - this.options.barWidthFillFraction)/2; } this.minxdelta = xdelta; this.bars = new Array(); var i = 0; for (var name in this.dataStores) { var store = this.dataStores[name]; if (typeof(store) == 'function') continue; for (var j = 0; j < store.length; j++) { var item = store[j]; var rect = { y: ((parseFloat(item[0]) - this.minxval) * this.xscale) + (i * barWidthForSet) + barMargin, x: 0.0, h: barWidthForSet, w: ((parseFloat(item[1]) - this.minyval) * this.yscale), xval: parseFloat(item[0]), yval: parseFloat(item[1]), name: name }; rect.y = (rect.y <= 0.0) ? 0.0 : (rect.y >= 1.0) ? 1.0 : rect.y; if ((rect.x >= 0.0) && (rect.x <= 1.0)) { this.bars.push(rect); } } i++; } }, /** * Evaluates bar ticks. * * @alias _evalBarTicks */ _evalBarTicks: function() { this._evalLineTicks(); this.xticks = this.xticks.collect(function(tick) { return [tick[0] + (this.minxdelta * this.xscale)/2, tick[1]]; }.bind(this)); if (this.options.barOrientation == 'horizontal') { var tmp = this.xticks; this.xticks = this.yticks.collect(function(tick) { return [1.0 - tick[0], tick[1]]; }.bind(this)); this.yticks = tmp; } }, /** * Renders a horizontal/vertical bar chart. * * @alias _renderBarChart */ _renderBarChart: function() { var cx = this.canvasNode.getContext('2d'); var index = 0; for(var storeName in this.dataStores) { var drawBar = function(bar) { if(bar.name != storeName || typeof(bar) == 'function') return; cx.save(); cx.lineWidth = this.options.strokeWidth; cx.fillStyle = this.options.colorScheme[index % this.options.colorScheme.length]; cx.strokeStyle = this.options.strokeColor; var x = this.area.w * bar.x + this.area.x; var y = this.area.h * bar.y + this.area.y; var w = this.area.w * bar.w; var h = this.area.h * bar.h; if ((w < 1) || (h < 1)) return; if (this.options.shouldFill) cx.fillRect(x, y, w, h); if (this.options.shouldStroke) cx.strokeRect(x, y, w, h); cx.restore(); }.bind(this); this.bars.each(drawBar); index++; } }, /** * Renders the axis for bar charts. * * @alias _renderBarAxis */ _renderBarAxis: function() { this._renderAxis(); } }); Plotr.LineChart = Class.create(); Object.extend(Plotr.LineChart.prototype, Plotr.Canvas); Object.extend(Plotr.LineChart.prototype, Plotr.Chart); Object.extend(Plotr.LineChart.prototype,{ /** * Type of chart we're dealing with. */ type: 'line', /** * Renders the chart with the specified options. The optional parameters * can be used to render a linechart in a different canvas element with new options. * * @alias render * @param {String} [element] - (optional) ID of a canvas element. * @param {Object} [options] - (optional) Options for rendering. */ render: function(element,options) { if(this.isIE && this._ieWaitForVML(element,options)){ return; } this._evaluate(options); this._render(element); this._renderLineChart(); this._renderLineAxis(); if(this.isIE) { for(var el in this.renderStack){ if(typeof(this.renderStack[el]) == 'function') break; this.render(el,this.renderStack[el]); break; } } }, /** * This function does all the math. It'll process all the data needed to * plot the chart. * * @alias evaluate * @param {Object} options - (optional) evaluate the chart with the given options. */ _evaluate: function(options) { this._eval(options); this._evalLineChart(); this._evalLineTicksForXAxis(); this._evalLineTicksForYAxis(); }, /** * Processes specific measures for line charts. * * @alias _evalLineChart */ _evalLineChart: function() { this.points = new Array(); for (var name in this.dataStores) { var store = this.dataStores[name]; if (typeof(store) == 'function') continue; for (var j = 0; j < store.length; j++) { var item = store[j]; var point = { x: ((parseFloat(item[0]) - this.minxval) * this.xscale), y: 1.0 - ((parseFloat(item[1]) - this.minyval) * this.yscale), xval: parseFloat(item[0]), yval: parseFloat(item[1]), name: name }; point.y = (point.y <= 0.0) ? 0.0 : (point.y >= 1.0) ? 1.0 : point.y; if ((point.x >= 0.0) && (point.x <= 1.0)) { this.points.push(point); } } } }, _renderLineChart: function() { var cx = this.canvasNode.getContext("2d"); var index = 0; for(var storeName in this.dataStores) { cx.save(); cx.lineWidth = this.options.strokeWidth; cx.fillStyle = this.options.colorScheme[index % this.options.colorScheme.length]; cx.strokeStyle = this.options.strokeColor; var preparePath = function() { cx.beginPath(); cx.moveTo(this.area.x, this.area.y + this.area.h); var addPoint = function(point) { }.bind(this); for(var point in this.points) { var currPoint = this.points[point]; if (currPoint.name == storeName) cx.lineTo(this.area.w * currPoint.x + this.area.x, this.area.h * currPoint.y + this.area.y); } cx.lineTo(this.area.w + this.area.x, this.area.h + this.area.y); cx.lineTo(this.area.x, this.area.y + this.area.h); cx.closePath(); }.bind(this); if (this.options.shouldFill) { preparePath(cx); cx.fill(); } if (this.options.shouldStroke) { preparePath(cx); cx.stroke(); } cx.restore(); index++; } } }); Plotr.PieChart = Class.create(); Object.extend(Plotr.PieChart.prototype, Plotr.Canvas); Object.extend(Plotr.PieChart.prototype, Plotr.Chart); Object.extend(Plotr.PieChart.prototype,{ /** * Type of chart we're dealing with. */ type: 'pie', /** * Renders the chart with the specified options. * * @alias render * @param {String} [element] - (optional) ID of a canvas element. * @param {Object} [options] - (optional) Options for rendering. */ render: function(element,options) { var isNil = Plotr.Base.isNil; if(this.isIE && this._ieWaitForVML(element,options)){ return; } this._evaluate(options); this._render(element); this._renderPieChart(); this._renderPieAxis(); if(this.isIE) { for(var el in this.renderStack){ if(typeof(this.renderStack[el]) == 'function') break; this.render(el,this.renderStack[el]); break; } } }, /** * This function does all the math. This function evaluates all the data needed * to plot the chart. * * @alias _evaluate * @param {Object} [options] - (optional) Evaluate the chart with the given options. */ _evaluate: function(options) { this._eval(options); this._evalPieChart(); this._evalPieTicks(); }, _evalPieChart: function(){ var store = this.stores[0]; var sum = Plotr.Base.sum(store.pluck(1)); var angle = 0.0; this.slices = new Array(); for(var i = 0, slice = null, fraction = null; i < store.length; i++){ slice = store[i]; if(slice[1] > 0){ fraction = slice[1]/sum; this.slices.push({ fraction: fraction, xval: slice[0], yval: slice[1], startAngle: 2 * angle * Math.PI, endAngle: 2 * (angle + fraction) * Math.PI }); angle += fraction; } } }, _renderPieChart: function(){ var cx = this.canvasNode.getContext('2d'); var centerx = this.area.x + this.area.w * 0.5; var centery = this.area.y + this.area.h * 0.5; var radius = Math.min(this.area.w * this.options.pieRadius, this.area.h * this.options.pieRadius); if(this.isIE) { centerx = parseInt(centerx); centery = parseInt(centery); radius = parseInt(radius); } for(var i = 0, ln = this.slices.length; i < ln; i++) { cx.save(); cx.fillStyle = this.options.colorScheme[i % this.options.colorScheme.length]; var drawPie = function(){ cx.beginPath(); cx.moveTo(centerx, centery); cx.arc(centerx, centery, radius, this.slices[i].startAngle - Math.PI/2, this.slices[i].endAngle - Math.PI/2, false); cx.lineTo(centerx, centery); cx.closePath(); }.bind(this); if(Math.abs(this.slices[i].startAngle - this.slices[i].endAngle) > 0.001) { if(this.options.shouldFill) { drawPie(); cx.fill(); } if (this.options.shouldStroke) { drawPie(); cx.lineWidth = this.options.strokeWidth; if (this.options.strokeColor) cx.strokeStyle = this.options.strokeColor; cx.stroke(); } } cx.restore(); } }, _evalPieTicks: function() { this.xticks = new Array(); var toPercentage = function(n){ n *= 100; return n.toFixed(1)+'%'; }; if(this.options.xTicks) { var lookup = new Array(); for (var i = 0; i < this.slices.length; i++) { lookup[this.slices[i].xval] = this.slices[i]; } for(var i = 0; i < this.options.xTicks.length; i++) { var tick = this.options.xTicks[i]; var slice = lookup[tick.v]; var label = tick.label; if(slice) { if (Plotr.Base.isNil(label)) label = tick.v.toString(); label += " (" + toPercentage(slice.fraction) + ")"; this.xticks.push([tick.v, label]); } } }else{ for(var i =0; i < this.slices.length; i++) { var slice = this.slices[i]; var label = slice.xval + " (" + toPercentage(slice.fraction) + ")"; this.xticks.push([slice.xval, label]); } } }, _renderPieAxis: function() { if (!this.options.drawXAxis) return; if(this.xticks){ var lookup = new Array(); for (var i = 0; i < this.slices.length; i++) { lookup[this.slices[i].xval] = this.slices[i]; } var centerx = this.area.x + this.area.w * 0.5; var centery = this.area.y + this.area.h * 0.5; var radius = Math.min(this.area.w * this.options.pieRadius, this.area.h * this.options.pieRadius); var labelWidth = this.options.axisLabelWidth; for(var i = 0; i < this.xticks.length; i++) { var slice = lookup[this.xticks[i][0]]; if(Plotr.Base.isNil(slice)) continue; var angle = (slice.startAngle + slice.endAngle)/2; // normalize the angle var normalisedAngle = angle; if (normalisedAngle > Math.PI * 2) normalisedAngle = normalisedAngle - Math.PI * 2; else if (normalisedAngle < 0) normalisedAngle = normalisedAngle + Math.PI * 2; var labelx = centerx + Math.sin(normalisedAngle) * (radius + 10); var labely = centery - Math.cos(normalisedAngle) * (radius + 10); var labelStyle = { position: 'absolute', zIndex: 11, width: labelWidth + 'px', fontFamily: this.options.axisLabelFont, fontSize: this.options.axisLabelFontSize + 'px', overflow: 'hidden', color: this.options.axisLabelColor }; if(normalisedAngle <= Math.PI * 0.5) { // text on top and align left Object.extend(labelStyle, { textAlign: 'left', verticalAlign: 'top', left: labelx + 'px', top: (labely - this.options.axisLabelFontSize) + 'px' }); }else if ((normalisedAngle > Math.PI * 0.5) && (normalisedAngle <= Math.PI)) { // text on bottom and align left Object.extend(labelStyle, { textAlign: 'left', verticalAlign: 'bottom', left: labelx + 'px', top: labely + 'px' }); }else if ((normalisedAngle > Math.PI) && (normalisedAngle <= Math.PI*1.5)) { // text on bottom and align right Object.extend(labelStyle, { textAlign: 'right', verticalAlign: 'bottom', left: (labelx - labelWidth) + 'px', top: labely + 'px' }); }else { // text on top and align right Object.extend(labelStyle, { textAlign: 'right', verticalAlign: 'bottom', left: (labelx - labelWidth) + 'px', top: (labely - this.options.axisLabelFontSize) + 'px' }); } var label = document.createElement('div'); label.appendChild(document.createTextNode(this.xticks[i][1])); Element.setStyle(label, labelStyle); this.containerNode.appendChild(label); this.xlabels.push(label); } } } });