(function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(['bean', 'underscore'], function (bean, _) { // Also create a global in case some scripts // that are loaded still are looking for // a global even when an AMD loader is in use. return (root.Flotr = factory(bean, _)); }); } else { // Browser globals root.Flotr = factory(root.bean, root._); } }(this, function (bean, _) { /** * Flotr2 (c) 2012 Carl Sutherland * MIT License * Special thanks to: * Flotr: http://code.google.com/p/flotr/ (fork) * Flot: https://github.com/flot/flot (original fork) */ (function () { var global = this, previousFlotr = this.Flotr, Flotr; Flotr = { _: _, bean: bean, isIphone: /iphone/i.test(navigator.userAgent), isIE: (navigator.appVersion.indexOf("MSIE") != -1 ? parseFloat(navigator.appVersion.split("MSIE")[1]) : false), /** * An object of the registered graph types. Use Flotr.addType(type, object) * to add your own type. */ graphTypes: {}, /** * The list of the registered plugins */ plugins: {}, /** * Can be used to add your own chart type. * @param {String} name - Type of chart, like 'pies', 'bars' etc. * @param {String} graphType - The object containing the basic drawing functions (draw, etc) */ addType: function(name, graphType){ Flotr.graphTypes[name] = graphType; Flotr.defaultOptions[name] = graphType.options || {}; Flotr.defaultOptions.defaultType = Flotr.defaultOptions.defaultType || name; }, /** * Can be used to add a plugin * @param {String} name - The name of the plugin * @param {String} plugin - The object containing the plugin's data (callbacks, options, function1, function2, ...) */ addPlugin: function(name, plugin){ Flotr.plugins[name] = plugin; Flotr.defaultOptions[name] = plugin.options || {}; }, /** * Draws the graph. This function is here for backwards compatibility with Flotr version 0.1.0alpha. * You could also draw graphs by directly calling Flotr.Graph(element, data, options). * @param {Element} el - element to insert the graph into * @param {Object} data - an array or object of dataseries * @param {Object} options - an object containing options * @param {Class} _GraphKlass_ - (optional) Class to pass the arguments to, defaults to Flotr.Graph * @return {Object} returns a new graph object and of course draws the graph. */ draw: function(el, data, options, GraphKlass){ GraphKlass = GraphKlass || Flotr.Graph; return new GraphKlass(el, data, options); }, /** * Recursively merges two objects. * @param {Object} src - source object (likely the object with the least properties) * @param {Object} dest - destination object (optional, object with the most properties) * @return {Object} recursively merged Object * @TODO See if we can't remove this. */ merge: function(src, dest){ var i, v, result = dest || {}; for (i in src) { v = src[i]; if (v && typeof(v) === 'object') { if (v.constructor === Array) { result[i] = this._.clone(v); } else if (v.constructor !== RegExp && !this._.isElement(v)) { result[i] = Flotr.merge(v, (dest ? dest[i] : undefined)); } else { result[i] = v; } } else { result[i] = v; } } return result; }, /** * Recursively clones an object. * @param {Object} object - The object to clone * @return {Object} the clone * @TODO See if we can't remove this. */ clone: function(object){ return Flotr.merge(object, {}); }, /** * Function calculates the ticksize and returns it. * @param {Integer} noTicks - number of ticks * @param {Integer} min - lower bound integer value for the current axis * @param {Integer} max - upper bound integer value for the current axis * @param {Integer} decimals - number of decimals for the ticks * @return {Integer} returns the ticksize in pixels */ getTickSize: function(noTicks, min, max, decimals){ var delta = (max - min) / noTicks, magn = Flotr.getMagnitude(delta), tickSize = 10, norm = delta / magn; // Norm is between 1.0 and 10.0. if(norm < 1.5) tickSize = 1; else if(norm < 2.25) tickSize = 2; else if(norm < 3) tickSize = ((decimals === 0) ? 2 : 2.5); else if(norm < 7.5) tickSize = 5; return tickSize * magn; }, /** * Default tick formatter. * @param {String, Integer} val - tick value integer * @param {Object} axisOpts - the axis' options * @return {String} formatted tick string */ defaultTickFormatter: function(val, axisOpts){ return val+''; }, /** * Formats the mouse tracker values. * @param {Object} obj - Track value Object {x:..,y:..} * @return {String} Formatted track string */ defaultTrackFormatter: function(obj){ return '('+obj.x+', '+obj.y+')'; }, /** * Utility function to convert file size values in bytes to kB, MB, ... * @param value {Number} - The value to convert * @param precision {Number} - The number of digits after the comma (default: 2) * @param base {Number} - The base (default: 1000) */ engineeringNotation: function(value, precision, base){ var sizes = ['Y','Z','E','P','T','G','M','k',''], fractionSizes = ['y','z','a','f','p','n','µ','m',''], total = sizes.length; base = base || 1000; precision = Math.pow(10, precision || 2); if (value === 0) return 0; if (value > 1) { while (total-- && (value >= base)) value /= base; } else { sizes = fractionSizes; total = sizes.length; while (total-- && (value < 1)) value *= base; } return (Math.round(value * precision) / precision) + sizes[total]; }, /** * Returns the magnitude of the input value. * @param {Integer, Float} x - integer or float value * @return {Integer, Float} returns the magnitude of the input value */ getMagnitude: function(x){ return Math.pow(10, Math.floor(Math.log(x) / Math.LN10)); }, toPixel: function(val){ return Math.floor(val)+0.5;//((val-Math.round(val) < 0.4) ? (Math.floor(val)-0.5) : val); }, toRad: function(angle){ return -angle * (Math.PI/180); }, floorInBase: function(n, base) { return base * Math.floor(n / base); }, drawText: function(ctx, text, x, y, style) { if (!ctx.fillText) { ctx.drawText(text, x, y, style); return; } style = this._.extend({ size: Flotr.defaultOptions.fontSize, color: '#000000', textAlign: 'left', textBaseline: 'bottom', weight: 1, angle: 0 }, style); ctx.save(); ctx.translate(x, y); ctx.rotate(style.angle); ctx.fillStyle = style.color; ctx.font = (style.weight > 1 ? "bold " : "") + (style.size*1.3) + "px sans-serif"; ctx.textAlign = style.textAlign; ctx.textBaseline = style.textBaseline; ctx.fillText(text, 0, 0); ctx.restore(); }, getBestTextAlign: function(angle, style) { style = style || {textAlign: 'center', textBaseline: 'middle'}; angle += Flotr.getTextAngleFromAlign(style); if (Math.abs(Math.cos(angle)) > 10e-3) style.textAlign = (Math.cos(angle) > 0 ? 'right' : 'left'); if (Math.abs(Math.sin(angle)) > 10e-3) style.textBaseline = (Math.sin(angle) > 0 ? 'top' : 'bottom'); return style; }, alignTable: { 'right middle' : 0, 'right top' : Math.PI/4, 'center top' : Math.PI/2, 'left top' : 3*(Math.PI/4), 'left middle' : Math.PI, 'left bottom' : -3*(Math.PI/4), 'center bottom': -Math.PI/2, 'right bottom' : -Math.PI/4, 'center middle': 0 }, getTextAngleFromAlign: function(style) { return Flotr.alignTable[style.textAlign+' '+style.textBaseline] || 0; }, noConflict : function () { global.Flotr = previousFlotr; return this; } }; global.Flotr = Flotr; })(); /** * Flotr Defaults */ Flotr.defaultOptions = { colors: ['#00A8F0', '#C0D800', '#CB4B4B', '#4DA74D', '#9440ED'], //=> The default colorscheme. When there are > 5 series, additional colors are generated. ieBackgroundColor: '#FFFFFF', // Background color for excanvas clipping title: null, // => The graph's title subtitle: null, // => The graph's subtitle shadowSize: 4, // => size of the 'fake' shadow defaultType: null, // => default series type HtmlText: true, // => wether to draw the text using HTML or on the canvas fontColor: '#545454', // => default font color fontSize: 7.5, // => canvas' text font size resolution: 1, // => resolution of the graph, to have printer-friendly graphs ! parseFloat: true, // => whether to preprocess data for floats (ie. if input is string) preventDefault: true, // => preventDefault by default for mobile events. Turn off to enable scroll. xaxis: { ticks: null, // => format: either [1, 3] or [[1, 'a'], 3] minorTicks: null, // => format: either [1, 3] or [[1, 'a'], 3] showLabels: true, // => setting to true will show the axis ticks labels, hide otherwise showMinorLabels: false,// => true to show the axis minor ticks labels, false to hide labelsAngle: 0, // => labels' angle, in degrees title: null, // => axis title titleAngle: 0, // => axis title's angle, in degrees noTicks: 5, // => number of ticks for automagically generated ticks minorTickFreq: null, // => number of minor ticks between major ticks for autogenerated ticks tickFormatter: Flotr.defaultTickFormatter, // => fn: number, Object -> string tickDecimals: null, // => no. of decimals, null means auto min: null, // => min. value to show, null means set automatically max: null, // => max. value to show, null means set automatically autoscale: false, // => Turns autoscaling on with true autoscaleMargin: 0, // => margin in % to add if auto-setting min/max color: null, // => color of the ticks mode: 'normal', // => can be 'time' or 'normal' timeFormat: null, timeMode:'UTC', // => For UTC time ('local' for local time). timeUnit:'millisecond',// => Unit for time (millisecond, second, minute, hour, day, month, year) scaling: 'linear', // => Scaling, can be 'linear' or 'logarithmic' base: Math.E, titleAlign: 'center', margin: true // => Turn off margins with false }, x2axis: {}, yaxis: { ticks: null, // => format: either [1, 3] or [[1, 'a'], 3] minorTicks: null, // => format: either [1, 3] or [[1, 'a'], 3] showLabels: true, // => setting to true will show the axis ticks labels, hide otherwise showMinorLabels: false,// => true to show the axis minor ticks labels, false to hide labelsAngle: 0, // => labels' angle, in degrees title: null, // => axis title titleAngle: 90, // => axis title's angle, in degrees noTicks: 5, // => number of ticks for automagically generated ticks minorTickFreq: null, // => number of minor ticks between major ticks for autogenerated ticks tickFormatter: Flotr.defaultTickFormatter, // => fn: number, Object -> string tickDecimals: null, // => no. of decimals, null means auto min: null, // => min. value to show, null means set automatically max: null, // => max. value to show, null means set automatically autoscale: false, // => Turns autoscaling on with true autoscaleMargin: 0, // => margin in % to add if auto-setting min/max color: null, // => The color of the ticks scaling: 'linear', // => Scaling, can be 'linear' or 'logarithmic' base: Math.E, titleAlign: 'center', margin: true // => Turn off margins with false }, y2axis: { titleAngle: 270 }, grid: { color: '#545454', // => primary color used for outline and labels backgroundColor: null, // => null for transparent, else color backgroundImage: null, // => background image. String or object with src, left and top watermarkAlpha: 0.4, // => tickColor: '#DDDDDD', // => color used for the ticks labelMargin: 3, // => margin in pixels verticalLines: true, // => whether to show gridlines in vertical direction minorVerticalLines: null, // => whether to show gridlines for minor ticks in vertical dir. horizontalLines: true, // => whether to show gridlines in horizontal direction minorHorizontalLines: null, // => whether to show gridlines for minor ticks in horizontal dir. outlineWidth: 1, // => width of the grid outline/border in pixels outline : 'nsew', // => walls of the outline to display circular: false // => if set to true, the grid will be circular, must be used when radars are drawn }, mouse: { track: false, // => true to track the mouse, no tracking otherwise trackAll: false, position: 'se', // => position of the value box (default south-east) relative: false, // => next to the mouse cursor trackFormatter: Flotr.defaultTrackFormatter, // => formats the values in the value box margin: 5, // => margin in pixels of the valuebox lineColor: '#FF3F19', // => line color of points that are drawn when mouse comes near a value of a series trackDecimals: 1, // => decimals for the track values sensibility: 2, // => the lower this number, the more precise you have to aim to show a value trackY: true, // => whether or not to track the mouse in the y axis radius: 3, // => radius of the track point fillColor: null, // => color to fill our select bar with only applies to bar and similar graphs (only bars for now) fillOpacity: 0.4 // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill } }; /** * Flotr Color */ (function () { var _ = Flotr._; // Constructor function Color (r, g, b, a) { this.rgba = ['r','g','b','a']; var x = 4; while(-1<--x){ this[this.rgba[x]] = arguments[x] || ((x==3) ? 1.0 : 0); } this.normalize(); } // Constants var COLOR_NAMES = { aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255], brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169], darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47], darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122], darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130], khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144], lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255], maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128], violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0] }; Color.prototype = { scale: function(rf, gf, bf, af){ var x = 4; while (-1 < --x) { if (!_.isUndefined(arguments[x])) this[this.rgba[x]] *= arguments[x]; } return this.normalize(); }, alpha: function(alpha) { if (!_.isUndefined(alpha) && !_.isNull(alpha)) { this.a = alpha; } return this.normalize(); }, clone: function(){ return new Color(this.r, this.b, this.g, this.a); }, limit: function(val,minVal,maxVal){ return Math.max(Math.min(val, maxVal), minVal); }, normalize: function(){ var limit = this.limit; this.r = limit(parseInt(this.r, 10), 0, 255); this.g = limit(parseInt(this.g, 10), 0, 255); this.b = limit(parseInt(this.b, 10), 0, 255); this.a = limit(this.a, 0, 1); return this; }, distance: function(color){ if (!color) return; color = new Color.parse(color); var dist = 0, x = 3; while(-1<--x){ dist += Math.abs(this[this.rgba[x]] - color[this.rgba[x]]); } return dist; }, toString: function(){ return (this.a >= 1.0) ? 'rgb('+[this.r,this.g,this.b].join(',')+')' : 'rgba('+[this.r,this.g,this.b,this.a].join(',')+')'; }, contrast: function () { var test = 1 - ( 0.299 * this.r + 0.587 * this.g + 0.114 * this.b) / 255; return (test < 0.5 ? '#000000' : '#ffffff'); } }; _.extend(Color, { /** * Parses a color string and returns a corresponding Color. * The different tests are in order of probability to improve speed. * @param {String, Color} str - string thats representing a color * @return {Color} returns a Color object or false */ parse: function(color){ if (color instanceof Color) return color; var result; // #a0b1c2 if((result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color))) return new Color(parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)); // rgb(num,num,num) if((result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color))) return new Color(parseInt(result[1], 10), parseInt(result[2], 10), parseInt(result[3], 10)); // #fff if((result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color))) return new Color(parseInt(result[1]+result[1],16), parseInt(result[2]+result[2],16), parseInt(result[3]+result[3],16)); // rgba(num,num,num,num) if((result = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(color))) return new Color(parseInt(result[1], 10), parseInt(result[2], 10), parseInt(result[3], 10), parseFloat(result[4])); // rgb(num%,num%,num%) if((result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(color))) return new Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55); // rgba(num%,num%,num%,num) if((result = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(color))) return new Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55, parseFloat(result[4])); // Otherwise, we're most likely dealing with a named color. var name = (color+'').replace(/^\s*([\S\s]*?)\s*$/, '$1').toLowerCase(); if(name == 'transparent'){ return new Color(255, 255, 255, 0); } return (result = COLOR_NAMES[name]) ? new Color(result[0], result[1], result[2]) : new Color(0, 0, 0, 0); }, /** * Process color and options into color style. */ processColor: function(color, options) { var opacity = options.opacity; if (!color) return 'rgba(0, 0, 0, 0)'; if (color instanceof Color) return color.alpha(opacity).toString(); if (_.isString(color)) return Color.parse(color).alpha(opacity).toString(); var grad = color.colors ? color : {colors: color}; if (!options.ctx) { if (!_.isArray(grad.colors)) return 'rgba(0, 0, 0, 0)'; return Color.parse(_.isArray(grad.colors[0]) ? grad.colors[0][1] : grad.colors[0]).alpha(opacity).toString(); } grad = _.extend({start: 'top', end: 'bottom'}, grad); if (/top/i.test(grad.start)) options.x1 = 0; if (/left/i.test(grad.start)) options.y1 = 0; if (/bottom/i.test(grad.end)) options.x2 = 0; if (/right/i.test(grad.end)) options.y2 = 0; var i, c, stop, gradient = options.ctx.createLinearGradient(options.x1, options.y1, options.x2, options.y2); for (i = 0; i < grad.colors.length; i++) { c = grad.colors[i]; if (_.isArray(c)) { stop = c[0]; c = c[1]; } else stop = i / (grad.colors.length-1); gradient.addColorStop(stop, Color.parse(c).alpha(opacity)); } return gradient; } }); Flotr.Color = Color; })(); /** * Flotr Date */ Flotr.Date = { set : function (date, name, mode, value) { mode = mode || 'UTC'; name = 'set' + (mode === 'UTC' ? 'UTC' : '') + name; date[name](value); }, get : function (date, name, mode) { mode = mode || 'UTC'; name = 'get' + (mode === 'UTC' ? 'UTC' : '') + name; return date[name](); }, format: function(d, format, mode) { if (!d) return; // We should maybe use an "official" date format spec, like PHP date() or ColdFusion // http://fr.php.net/manual/en/function.date.php // http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=functions_c-d_29.html var get = this.get, tokens = { h: get(d, 'Hours', mode).toString(), H: leftPad(get(d, 'Hours', mode)), M: leftPad(get(d, 'Minutes', mode)), S: leftPad(get(d, 'Seconds', mode)), s: get(d, 'Milliseconds', mode), d: get(d, 'Date', mode).toString(), m: (get(d, 'Month', mode) + 1).toString(), y: get(d, 'FullYear', mode).toString(), b: Flotr.Date.monthNames[get(d, 'Month', mode)] }; function leftPad(n){ n += ''; return n.length == 1 ? "0" + n : n; } var r = [], c, escape = false; for (var i = 0; i < format.length; ++i) { c = format.charAt(i); if (escape) { r.push(tokens[c] || c); escape = false; } else if (c == "%") escape = true; else r.push(c); } return r.join(''); }, getFormat: function(time, span) { var tu = Flotr.Date.timeUnits; if (time < tu.second) return "%h:%M:%S.%s"; else if (time < tu.minute) return "%h:%M:%S"; else if (time < tu.day) return (span < 2 * tu.day) ? "%h:%M" : "%b %d %h:%M"; else if (time < tu.month) return "%b %d"; else if (time < tu.year) return (span < tu.year) ? "%b" : "%b %y"; else return "%y"; }, formatter: function (v, axis) { var options = axis.options, scale = Flotr.Date.timeUnits[options.timeUnit], d = new Date(v * scale); // first check global format if (axis.options.timeFormat) return Flotr.Date.format(d, options.timeFormat, options.timeMode); var span = (axis.max - axis.min) * scale, t = axis.tickSize * Flotr.Date.timeUnits[axis.tickUnit]; return Flotr.Date.format(d, Flotr.Date.getFormat(t, span), options.timeMode); }, generator: function(axis) { var set = this.set, get = this.get, timeUnits = this.timeUnits, spec = this.spec, options = axis.options, mode = options.timeMode, scale = timeUnits[options.timeUnit], min = axis.min * scale, max = axis.max * scale, delta = (max - min) / options.noTicks, ticks = [], tickSize = axis.tickSize, tickUnit, formatter, i; // Use custom formatter or time tick formatter formatter = (options.tickFormatter === Flotr.defaultTickFormatter ? this.formatter : options.tickFormatter ); for (i = 0; i < spec.length - 1; ++i) { var d = spec[i][0] * timeUnits[spec[i][1]]; if (delta < (d + spec[i+1][0] * timeUnits[spec[i+1][1]]) / 2 && d >= tickSize) break; } tickSize = spec[i][0]; tickUnit = spec[i][1]; // special-case the possibility of several years if (tickUnit == "year") { tickSize = Flotr.getTickSize(options.noTicks*timeUnits.year, min, max, 0); // Fix for 0.5 year case if (tickSize == 0.5) { tickUnit = "month"; tickSize = 6; } } axis.tickUnit = tickUnit; axis.tickSize = tickSize; var step = tickSize * timeUnits[tickUnit]; d = new Date(min); function setTick (name) { set(d, name, mode, Flotr.floorInBase( get(d, name, mode), tickSize )); } switch (tickUnit) { case "millisecond": setTick('Milliseconds'); break; case "second": setTick('Seconds'); break; case "minute": setTick('Minutes'); break; case "hour": setTick('Hours'); break; case "month": setTick('Month'); break; case "year": setTick('FullYear'); break; } // reset smaller components if (step >= timeUnits.second) set(d, 'Milliseconds', mode, 0); if (step >= timeUnits.minute) set(d, 'Seconds', mode, 0); if (step >= timeUnits.hour) set(d, 'Minutes', mode, 0); if (step >= timeUnits.day) set(d, 'Hours', mode, 0); if (step >= timeUnits.day * 4) set(d, 'Date', mode, 1); if (step >= timeUnits.year) set(d, 'Month', mode, 0); var carry = 0, v = NaN, prev; do { prev = v; v = d.getTime(); ticks.push({ v: v / scale, label: formatter(v / scale, axis) }); if (tickUnit == "month") { if (tickSize < 1) { /* a bit complicated - we'll divide the month up but we need to take care of fractions so we don't end up in the middle of a day */ set(d, 'Date', mode, 1); var start = d.getTime(); set(d, 'Month', mode, get(d, 'Month', mode) + 1); var end = d.getTime(); d.setTime(v + carry * timeUnits.hour + (end - start) * tickSize); carry = get(d, 'Hours', mode); set(d, 'Hours', mode, 0); } else set(d, 'Month', mode, get(d, 'Month', mode) + tickSize); } else if (tickUnit == "year") { set(d, 'FullYear', mode, get(d, 'FullYear', mode) + tickSize); } else d.setTime(v + step); } while (v < max && v != prev); return ticks; }, timeUnits: { millisecond: 1, second: 1000, minute: 1000 * 60, hour: 1000 * 60 * 60, day: 1000 * 60 * 60 * 24, month: 1000 * 60 * 60 * 24 * 30, year: 1000 * 60 * 60 * 24 * 365.2425 }, // the allowed tick sizes, after 1 year we use an integer algorithm spec: [ [1, "millisecond"], [20, "millisecond"], [50, "millisecond"], [100, "millisecond"], [200, "millisecond"], [500, "millisecond"], [1, "second"], [2, "second"], [5, "second"], [10, "second"], [30, "second"], [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], [30, "minute"], [1, "hour"], [2, "hour"], [4, "hour"], [8, "hour"], [12, "hour"], [1, "day"], [2, "day"], [3, "day"], [0.25, "month"], [0.5, "month"], [1, "month"], [2, "month"], [3, "month"], [6, "month"], [1, "year"] ], monthNames: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] }; (function () { var _ = Flotr._; Flotr.DOM = { addClass: function(element, name){ var classList = (element.className ? element.className : ''); if (_.include(classList.split(/\s+/g), name)) return; element.className = (classList ? classList + ' ' : '') + name; }, /** * Create an element. */ create: function(tag){ return document.createElement(tag); }, node: function(html) { var div = Flotr.DOM.create('div'), n; div.innerHTML = html; n = div.children[0]; div.innerHTML = ''; return n; }, /** * Remove all children. */ empty: function(element){ element.innerHTML = ''; /* if (!element) return; _.each(element.childNodes, function (e) { Flotr.DOM.empty(e); element.removeChild(e); }); */ }, hide: function(element){ Flotr.DOM.setStyles(element, {display:'none'}); }, /** * Insert a child. * @param {Element} element * @param {Element|String} Element or string to be appended. */ insert: function(element, child){ if(_.isString(child)) element.innerHTML += child; else if (_.isElement(child)) element.appendChild(child); }, // @TODO find xbrowser implementation opacity: function(element, opacity) { element.style.opacity = opacity; }, position: function(element, p){ if (!element.offsetParent) return {left: (element.offsetLeft || 0), top: (element.offsetTop || 0)}; p = this.position(element.offsetParent); p.left += element.offsetLeft; p.top += element.offsetTop; return p; }, removeClass: function(element, name) { var classList = (element.className ? element.className : ''); element.className = _.filter(classList.split(/\s+/g), function (c) { if (c != name) return true; } ).join(' '); }, setStyles: function(element, o) { _.each(o, function (value, key) { element.style[key] = value; }); }, show: function(element){ Flotr.DOM.setStyles(element, {display:''}); }, /** * Return element size. */ size: function(element){ return { height : element.offsetHeight, width : element.offsetWidth }; } }; })(); /** * Flotr Event Adapter */ (function () { var F = Flotr, bean = F.bean; F.EventAdapter = { observe: function(object, name, callback) { bean.add(object, name, callback); return this; }, fire: function(object, name, args) { bean.fire(object, name, args); if (typeof(Prototype) != 'undefined') Event.fire(object, name, args); // @TODO Someone who uses mootools, add mootools adapter for existing applciations. return this; }, stopObserving: function(object, name, callback) { bean.remove(object, name, callback); return this; }, eventPointer: function(e) { if (!F._.isUndefined(e.touches) && e.touches.length > 0) { return { x : e.touches[0].pageX, y : e.touches[0].pageY }; } else if (!F._.isUndefined(e.changedTouches) && e.changedTouches.length > 0) { return { x : e.changedTouches[0].pageX, y : e.changedTouches[0].pageY }; } else if (e.pageX || e.pageY) { return { x : e.pageX, y : e.pageY }; } else if (e.clientX || e.clientY) { var d = document, b = d.body, de = d.documentElement; return { x: e.clientX + b.scrollLeft + de.scrollLeft, y: e.clientY + b.scrollTop + de.scrollTop }; } } }; })(); /** * Text Utilities */ (function () { var F = Flotr, D = F.DOM, _ = F._, Text = function (o) { this.o = o; }; Text.prototype = { dimensions : function (text, canvasStyle, htmlStyle, className) { if (!text) return { width : 0, height : 0 }; return (this.o.html) ? this.html(text, this.o.element, htmlStyle, className) : this.canvas(text, canvasStyle); }, canvas : function (text, style) { if (!this.o.textEnabled) return; style = style || {}; var metrics = this.measureText(text, style), width = metrics.width, height = style.size || F.defaultOptions.fontSize, angle = style.angle || 0, cosAngle = Math.cos(angle), sinAngle = Math.sin(angle), widthPadding = 2, heightPadding = 6, bounds; bounds = { width: Math.abs(cosAngle * width) + Math.abs(sinAngle * height) + widthPadding, height: Math.abs(sinAngle * width) + Math.abs(cosAngle * height) + heightPadding }; return bounds; }, html : function (text, element, style, className) { var div = D.create('div'); D.setStyles(div, { 'position' : 'absolute', 'top' : '-10000px' }); D.insert(div, '
' + text + '
'); D.insert(this.o.element, div); return D.size(div); }, measureText : function (text, style) { var context = this.o.ctx, metrics; if (!context.fillText || (F.isIphone && context.measure)) { return { width : context.measure(text, style)}; } style = _.extend({ size: F.defaultOptions.fontSize, weight: 1, angle: 0 }, style); context.save(); context.font = (style.weight > 1 ? "bold " : "") + (style.size*1.3) + "px sans-serif"; metrics = context.measureText(text); context.restore(); return metrics; } }; Flotr.Text = Text; })(); /** * Flotr Graph class that plots a graph on creation. */ (function () { var D = Flotr.DOM, E = Flotr.EventAdapter, _ = Flotr._, flotr = Flotr; /** * Flotr Graph constructor. * @param {Element} el - element to insert the graph into * @param {Object} data - an array or object of dataseries * @param {Object} options - an object containing options */ Graph = function(el, data, options){ // Let's see if we can get away with out this [JS] // try { this._setEl(el); this._initMembers(); this._initPlugins(); E.fire(this.el, 'flotr:beforeinit', [this]); this.data = data; this.series = flotr.Series.getSeries(data); this._initOptions(options); this._initGraphTypes(); this._initCanvas(); this._text = new flotr.Text({ element : this.el, ctx : this.ctx, html : this.options.HtmlText, textEnabled : this.textEnabled }); E.fire(this.el, 'flotr:afterconstruct', [this]); this._initEvents(); this.findDataRanges(); this.calculateSpacing(); this.draw(_.bind(function() { E.fire(this.el, 'flotr:afterinit', [this]); }, this)); /* try { } catch (e) { try { console.error(e); } catch (e2) {} }*/ }; function observe (object, name, callback) { E.observe.apply(this, arguments); this._handles.push(arguments); return this; } Graph.prototype = { destroy: function () { E.fire(this.el, 'flotr:destroy'); _.each(this._handles, function (handle) { E.stopObserving.apply(this, handle); }); this._handles = []; this.el.graph = null; }, observe : observe, /** * @deprecated */ _observe : observe, processColor: function(color, options){ var o = { x1: 0, y1: 0, x2: this.plotWidth, y2: this.plotHeight, opacity: 1, ctx: this.ctx }; _.extend(o, options); return flotr.Color.processColor(color, o); }, /** * Function determines the min and max values for the xaxis and yaxis. * * TODO logarithmic range validation (consideration of 0) */ findDataRanges: function(){ var a = this.axes, xaxis, yaxis, range; _.each(this.series, function (series) { range = series.getRange(); if (range) { xaxis = series.xaxis; yaxis = series.yaxis; xaxis.datamin = Math.min(range.xmin, xaxis.datamin); xaxis.datamax = Math.max(range.xmax, xaxis.datamax); yaxis.datamin = Math.min(range.ymin, yaxis.datamin); yaxis.datamax = Math.max(range.ymax, yaxis.datamax); xaxis.used = (xaxis.used || range.xused); yaxis.used = (yaxis.used || range.yused); } }, this); // Check for empty data, no data case (none used) if (!a.x.used && !a.x2.used) a.x.used = true; if (!a.y.used && !a.y2.used) a.y.used = true; _.each(a, function (axis) { axis.calculateRange(); }); var types = _.keys(flotr.graphTypes), drawn = false; _.each(this.series, function (series) { if (series.hide) return; _.each(types, function (type) { if (series[type] && series[type].show) { this.extendRange(type, series); drawn = true; } }, this); if (!drawn) { this.extendRange(this.options.defaultType, series); } }, this); }, extendRange : function (type, series) { if (this[type].extendRange) this[type].extendRange(series, series.data, series[type], this[type]); if (this[type].extendYRange) this[type].extendYRange(series.yaxis, series.data, series[type], this[type]); if (this[type].extendXRange) this[type].extendXRange(series.xaxis, series.data, series[type], this[type]); }, /** * Calculates axis label sizes. */ calculateSpacing: function(){ var a = this.axes, options = this.options, series = this.series, margin = options.grid.labelMargin, T = this._text, x = a.x, x2 = a.x2, y = a.y, y2 = a.y2, maxOutset = options.grid.outlineWidth, i, j, l, dim; // TODO post refactor, fix this _.each(a, function (axis) { axis.calculateTicks(); axis.calculateTextDimensions(T, options); }); // Title height dim = T.dimensions( options.title, {size: options.fontSize*1.5}, 'font-size:1em;font-weight:bold;', 'flotr-title' ); this.titleHeight = dim.height; // Subtitle height dim = T.dimensions( options.subtitle, {size: options.fontSize}, 'font-size:smaller;', 'flotr-subtitle' ); this.subtitleHeight = dim.height; for(j = 0; j < options.length; ++j){ if (series[j].points.show){ maxOutset = Math.max(maxOutset, series[j].points.radius + series[j].points.lineWidth/2); } } var p = this.plotOffset; if (x.options.margin === false) { p.bottom = 0; p.top = 0; } else { p.bottom += (options.grid.circular ? 0 : (x.used && x.options.showLabels ? (x.maxLabel.height + margin) : 0)) + (x.used && x.options.title ? (x.titleSize.height + margin) : 0) + maxOutset; p.top += (options.grid.circular ? 0 : (x2.used && x2.options.showLabels ? (x2.maxLabel.height + margin) : 0)) + (x2.used && x2.options.title ? (x2.titleSize.height + margin) : 0) + this.subtitleHeight + this.titleHeight + maxOutset; } if (y.options.margin === false) { p.left = 0; p.right = 0; } else { p.left += (options.grid.circular ? 0 : (y.used && y.options.showLabels ? (y.maxLabel.width + margin) : 0)) + (y.used && y.options.title ? (y.titleSize.width + margin) : 0) + maxOutset; p.right += (options.grid.circular ? 0 : (y2.used && y2.options.showLabels ? (y2.maxLabel.width + margin) : 0)) + (y2.used && y2.options.title ? (y2.titleSize.width + margin) : 0) + maxOutset; } p.top = Math.floor(p.top); // In order the outline not to be blured this.plotWidth = this.canvasWidth - p.left - p.right; this.plotHeight = this.canvasHeight - p.bottom - p.top; // TODO post refactor, fix this x.length = x2.length = this.plotWidth; y.length = y2.length = this.plotHeight; y.offset = y2.offset = this.plotHeight; x.setScale(); x2.setScale(); y.setScale(); y2.setScale(); }, /** * Draws grid, labels, series and outline. */ draw: function(after) { var context = this.ctx, i; E.fire(this.el, 'flotr:beforedraw', [this.series, this]); if (this.series.length) { context.save(); context.translate(this.plotOffset.left, this.plotOffset.top); for (i = 0; i < this.series.length; i++) { if (!this.series[i].hide) this.drawSeries(this.series[i]); } context.restore(); this.clip(); } E.fire(this.el, 'flotr:afterdraw', [this.series, this]); if (after) after(); }, /** * Actually draws the graph. * @param {Object} series - series to draw */ drawSeries: function(series){ function drawChart (series, typeKey) { var options = this.getOptions(series, typeKey); this[typeKey].draw(options); } var drawn = false; series = series || this.series; _.each(flotr.graphTypes, function (type, typeKey) { if (series[typeKey] && series[typeKey].show && this[typeKey]) { drawn = true; drawChart.call(this, series, typeKey); } }, this); if (!drawn) drawChart.call(this, series, this.options.defaultType); }, getOptions : function (series, typeKey) { var type = series[typeKey], graphType = this[typeKey], xaxis = series.xaxis, yaxis = series.yaxis, options = { context : this.ctx, width : this.plotWidth, height : this.plotHeight, fontSize : this.options.fontSize, fontColor : this.options.fontColor, textEnabled : this.textEnabled, htmlText : this.options.HtmlText, text : this._text, // TODO Is this necessary? element : this.el, data : series.data, color : series.color, shadowSize : series.shadowSize, xScale : xaxis.d2p, yScale : yaxis.d2p, xInverse : xaxis.p2d, yInverse : yaxis.p2d }; options = flotr.merge(type, options); // Fill options.fillStyle = this.processColor( type.fillColor || series.color, {opacity: type.fillOpacity} ); return options; }, /** * Calculates the coordinates from a mouse event object. * @param {Event} event - Mouse Event object. * @return {Object} Object with coordinates of the mouse. */ getEventPosition: function (e){ var d = document, b = d.body, de = d.documentElement, axes = this.axes, plotOffset = this.plotOffset, lastMousePos = this.lastMousePos, pointer = E.eventPointer(e), dx = pointer.x - lastMousePos.pageX, dy = pointer.y - lastMousePos.pageY, r, rx, ry; if ('ontouchstart' in this.el) { r = D.position(this.overlay); rx = pointer.x - r.left - plotOffset.left; ry = pointer.y - r.top - plotOffset.top; } else { r = this.overlay.getBoundingClientRect(); rx = e.clientX - r.left - plotOffset.left - b.scrollLeft - de.scrollLeft; ry = e.clientY - r.top - plotOffset.top - b.scrollTop - de.scrollTop; } return { x: axes.x.p2d(rx), x2: axes.x2.p2d(rx), y: axes.y.p2d(ry), y2: axes.y2.p2d(ry), relX: rx, relY: ry, dX: dx, dY: dy, absX: pointer.x, absY: pointer.y, pageX: pointer.x, pageY: pointer.y }; }, /** * Observes the 'click' event and fires the 'flotr:click' event. * @param {Event} event - 'click' Event object. */ clickHandler: function(event){ if(this.ignoreClick){ this.ignoreClick = false; return this.ignoreClick; } E.fire(this.el, 'flotr:click', [this.getEventPosition(event), this]); }, /** * Observes mouse movement over the graph area. Fires the 'flotr:mousemove' event. * @param {Event} event - 'mousemove' Event object. */ mouseMoveHandler: function(event){ if (this.mouseDownMoveHandler) return; var pos = this.getEventPosition(event); E.fire(this.el, 'flotr:mousemove', [event, pos, this]); this.lastMousePos = pos; }, /** * Observes the 'mousedown' event. * @param {Event} event - 'mousedown' Event object. */ mouseDownHandler: function (event){ /* // @TODO Context menu? if(event.isRightClick()) { event.stop(); var overlay = this.overlay; overlay.hide(); function cancelContextMenu () { overlay.show(); E.stopObserving(document, 'mousemove', cancelContextMenu); } E.observe(document, 'mousemove', cancelContextMenu); return; } */ if (this.mouseUpHandler) return; this.mouseUpHandler = _.bind(function (e) { E.stopObserving(document, 'mouseup', this.mouseUpHandler); E.stopObserving(document, 'mousemove', this.mouseDownMoveHandler); this.mouseDownMoveHandler = null; this.mouseUpHandler = null; // @TODO why? //e.stop(); E.fire(this.el, 'flotr:mouseup', [e, this]); }, this); this.mouseDownMoveHandler = _.bind(function (e) { var pos = this.getEventPosition(e); E.fire(this.el, 'flotr:mousemove', [event, pos, this]); this.lastMousePos = pos; }, this); E.observe(document, 'mouseup', this.mouseUpHandler); E.observe(document, 'mousemove', this.mouseDownMoveHandler); E.fire(this.el, 'flotr:mousedown', [event, this]); this.ignoreClick = false; }, drawTooltip: function(content, x, y, options) { var mt = this.getMouseTrack(), style = 'opacity:0.7;background-color:#000;color:#fff;display:none;position:absolute;padding:2px 8px;-moz-border-radius:4px;border-radius:4px;white-space:nowrap;', p = options.position, m = options.margin, plotOffset = this.plotOffset; if(x !== null && y !== null){ if (!options.relative) { // absolute to the canvas if(p.charAt(0) == 'n') style += 'top:' + (m + plotOffset.top) + 'px;bottom:auto;'; else if(p.charAt(0) == 's') style += 'bottom:' + (m + plotOffset.bottom) + 'px;top:auto;'; if(p.charAt(1) == 'e') style += 'right:' + (m + plotOffset.right) + 'px;left:auto;'; else if(p.charAt(1) == 'w') style += 'left:' + (m + plotOffset.left) + 'px;right:auto;'; } else { // relative to the mouse if(p.charAt(0) == 'n') style += 'bottom:' + (m - plotOffset.top - y + this.canvasHeight) + 'px;top:auto;'; else if(p.charAt(0) == 's') style += 'top:' + (m + plotOffset.top + y) + 'px;bottom:auto;'; if(p.charAt(1) == 'e') style += 'left:' + (m + plotOffset.left + x) + 'px;right:auto;'; else if(p.charAt(1) == 'w') style += 'right:' + (m - plotOffset.left - x + this.canvasWidth) + 'px;left:auto;'; } mt.style.cssText = style; D.empty(mt); D.insert(mt, content); D.show(mt); } else { D.hide(mt); } }, clip: function (ctx) { var o = this.plotOffset, w = this.canvasWidth, h = this.canvasHeight; ctx = ctx || this.ctx; if (flotr.isIE && flotr.isIE < 9) { // Clipping for excanvas :-( ctx.save(); ctx.fillStyle = this.processColor(this.options.ieBackgroundColor); ctx.fillRect(0, 0, w, o.top); ctx.fillRect(0, 0, o.left, h); ctx.fillRect(0, h - o.bottom, w, o.bottom); ctx.fillRect(w - o.right, 0, o.right,h); ctx.restore(); } else { ctx.clearRect(0, 0, w, o.top); ctx.clearRect(0, 0, o.left, h); ctx.clearRect(0, h - o.bottom, w, o.bottom); ctx.clearRect(w - o.right, 0, o.right,h); } }, _initMembers: function() { this._handles = []; this.lastMousePos = {pageX: null, pageY: null }; this.plotOffset = {left: 0, right: 0, top: 0, bottom: 0}; this.ignoreClick = true; this.prevHit = null; }, _initGraphTypes: function() { _.each(flotr.graphTypes, function(handler, graphType){ this[graphType] = flotr.clone(handler); }, this); }, _initEvents: function () { var el = this.el, touchendHandler, movement, touchend; if ('ontouchstart' in el) { touchendHandler = _.bind(function (e) { touchend = true; E.stopObserving(document, 'touchend', touchendHandler); E.fire(el, 'flotr:mouseup', [event, this]); this.multitouches = null; if (!movement) { this.clickHandler(e); } }, this); this.observe(this.overlay, 'touchstart', _.bind(function (e) { movement = false; touchend = false; this.ignoreClick = false; if (e.touches && e.touches.length > 1) { this.multitouches = e.touches; } E.fire(el, 'flotr:mousedown', [event, this]); this.observe(document, 'touchend', touchendHandler); }, this)); this.observe(this.overlay, 'touchmove', _.bind(function (e) { var pos = this.getEventPosition(e); if (this.options.preventDefault) { e.preventDefault(); } movement = true; if (this.multitouches || (e.touches && e.touches.length > 1)) { this.multitouches = e.touches; } else { if (!touchend) { E.fire(el, 'flotr:mousemove', [event, pos, this]); } } this.lastMousePos = pos; }, this)); } else { this. observe(this.overlay, 'mousedown', _.bind(this.mouseDownHandler, this)). observe(el, 'mousemove', _.bind(this.mouseMoveHandler, this)). observe(this.overlay, 'click', _.bind(this.clickHandler, this)). observe(el, 'mouseout', function () { E.fire(el, 'flotr:mouseout'); }); } }, /** * Initializes the canvas and it's overlay canvas element. When the browser is IE, this makes use * of excanvas. The overlay canvas is inserted for displaying interactions. After the canvas elements * are created, the elements are inserted into the container element. */ _initCanvas: function(){ var el = this.el, o = this.options, children = el.children, removedChildren = [], child, i, size, style; // Empty the el for (i = children.length; i--;) { child = children[i]; if (!this.canvas && child.className === 'flotr-canvas') { this.canvas = child; } else if (!this.overlay && child.className === 'flotr-overlay') { this.overlay = child; } else { removedChildren.push(child); } } for (i = removedChildren.length; i--;) { el.removeChild(removedChildren[i]); } D.setStyles(el, {position: 'relative'}); // For positioning labels and overlay. size = {}; size.width = el.clientWidth; size.height = el.clientHeight; if(size.width <= 0 || size.height <= 0 || o.resolution <= 0){ throw 'Invalid dimensions for plot, width = ' + size.width + ', height = ' + size.height + ', resolution = ' + o.resolution; } // Main canvas for drawing graph types this.canvas = getCanvas(this.canvas, 'canvas'); // Overlay canvas for interactive features this.overlay = getCanvas(this.overlay, 'overlay'); this.ctx = getContext(this.canvas); this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.octx = getContext(this.overlay); this.octx.clearRect(0, 0, this.overlay.width, this.overlay.height); this.canvasHeight = size.height; this.canvasWidth = size.width; this.textEnabled = !!this.ctx.drawText || !!this.ctx.fillText; // Enable text functions function getCanvas(canvas, name){ if(!canvas){ canvas = D.create('canvas'); if (typeof FlashCanvas != "undefined" && typeof canvas.getContext === 'function') { FlashCanvas.initElement(canvas); } canvas.className = 'flotr-'+name; canvas.style.cssText = 'position:absolute;left:0px;top:0px;'; D.insert(el, canvas); } _.each(size, function(size, attribute){ D.show(canvas); if (name == 'canvas' && canvas.getAttribute(attribute) === size) { return; } canvas.setAttribute(attribute, size * o.resolution); canvas.style[attribute] = size + 'px'; }); canvas.context_ = null; // Reset the ExCanvas context return canvas; } function getContext(canvas){ if(window.G_vmlCanvasManager) window.G_vmlCanvasManager.initElement(canvas); // For ExCanvas var context = canvas.getContext('2d'); if(!window.G_vmlCanvasManager) context.scale(o.resolution, o.resolution); return context; } }, _initPlugins: function(){ // TODO Should be moved to flotr and mixed in. _.each(flotr.plugins, function(plugin, name){ _.each(plugin.callbacks, function(fn, c){ this.observe(this.el, c, _.bind(fn, this)); }, this); this[name] = flotr.clone(plugin); _.each(this[name], function(fn, p){ if (_.isFunction(fn)) this[name][p] = _.bind(fn, this); }, this); }, this); }, /** * Sets options and initializes some variables and color specific values, used by the constructor. * @param {Object} opts - options object */ _initOptions: function(opts){ var options = flotr.clone(flotr.defaultOptions); options.x2axis = _.extend(_.clone(options.xaxis), options.x2axis); options.y2axis = _.extend(_.clone(options.yaxis), options.y2axis); this.options = flotr.merge(opts || {}, options); if (this.options.grid.minorVerticalLines === null && this.options.xaxis.scaling === 'logarithmic') { this.options.grid.minorVerticalLines = true; } if (this.options.grid.minorHorizontalLines === null && this.options.yaxis.scaling === 'logarithmic') { this.options.grid.minorHorizontalLines = true; } E.fire(this.el, 'flotr:afterinitoptions', [this]); this.axes = flotr.Axis.getAxes(this.options); // Initialize some variables used throughout this function. var assignedColors = [], colors = [], ln = this.series.length, neededColors = this.series.length, oc = this.options.colors, usedColors = [], variation = 0, c, i, j, s; // Collect user-defined colors from series. for(i = neededColors - 1; i > -1; --i){ c = this.series[i].color; if(c){ --neededColors; if(_.isNumber(c)) assignedColors.push(c); else usedColors.push(flotr.Color.parse(c)); } } // Calculate the number of colors that need to be generated. for(i = assignedColors.length - 1; i > -1; --i) neededColors = Math.max(neededColors, assignedColors[i] + 1); // Generate needed number of colors. for(i = 0; colors.length < neededColors;){ c = (oc.length == i) ? new flotr.Color(100, 100, 100) : flotr.Color.parse(oc[i]); // Make sure each serie gets a different color. var sign = variation % 2 == 1 ? -1 : 1, factor = 1 + sign * Math.ceil(variation / 2) * 0.2; c.scale(factor, factor, factor); /** * @todo if we're getting too close to something else, we should probably skip this one */ colors.push(c); if(++i >= oc.length){ i = 0; ++variation; } } // Fill the options with the generated colors. for(i = 0, j = 0; i < ln; ++i){ s = this.series[i]; // Assign the color. if (!s.color){ s.color = colors[j++].toString(); }else if(_.isNumber(s.color)){ s.color = colors[s.color].toString(); } // Every series needs an axis if (!s.xaxis) s.xaxis = this.axes.x; if (s.xaxis == 1) s.xaxis = this.axes.x; else if (s.xaxis == 2) s.xaxis = this.axes.x2; if (!s.yaxis) s.yaxis = this.axes.y; if (s.yaxis == 1) s.yaxis = this.axes.y; else if (s.yaxis == 2) s.yaxis = this.axes.y2; // Apply missing options to the series. for (var t in flotr.graphTypes){ s[t] = _.extend(_.clone(this.options[t]), s[t]); } s.mouse = _.extend(_.clone(this.options.mouse), s.mouse); if (_.isUndefined(s.shadowSize)) s.shadowSize = this.options.shadowSize; } }, _setEl: function(el) { if (!el) throw 'The target container doesn\'t exist'; else if (el.graph instanceof Graph) el.graph.destroy(); else if (!el.clientWidth) throw 'The target container must be visible'; el.graph = this; this.el = el; } }; Flotr.Graph = Graph; })(); /** * Flotr Axis Library */ (function () { var _ = Flotr._, LOGARITHMIC = 'logarithmic'; function Axis (o) { this.orientation = 1; this.offset = 0; this.datamin = Number.MAX_VALUE; this.datamax = -Number.MAX_VALUE; _.extend(this, o); } // Prototype Axis.prototype = { setScale : function () { var length = this.length, max = this.max, min = this.min, offset = this.offset, orientation = this.orientation, options = this.options, logarithmic = options.scaling === LOGARITHMIC, scale; if (logarithmic) { scale = length / (log(max, options.base) - log(min, options.base)); } else { scale = length / (max - min); } this.scale = scale; // Logarithmic? if (logarithmic) { this.d2p = function (dataValue) { return offset + orientation * (log(dataValue, options.base) - log(min, options.base)) * scale; } this.p2d = function (pointValue) { return exp((offset + orientation * pointValue) / scale + log(min, options.base), options.base); } } else { this.d2p = function (dataValue) { return offset + orientation * (dataValue - min) * scale; } this.p2d = function (pointValue) { return (offset + orientation * pointValue) / scale + min; } } }, calculateTicks : function () { var options = this.options; this.ticks = []; this.minorTicks = []; // User Ticks if(options.ticks){ this._cleanUserTicks(options.ticks, this.ticks); this._cleanUserTicks(options.minorTicks || [], this.minorTicks); } else { if (options.mode == 'time') { this._calculateTimeTicks(); } else if (options.scaling === 'logarithmic') { this._calculateLogTicks(); } else { this._calculateTicks(); } } // Ticks to strings _.each(this.ticks, function (tick) { tick.label += ''; }); _.each(this.minorTicks, function (tick) { tick.label += ''; }); }, /** * Calculates the range of an axis to apply autoscaling. */ calculateRange: function () { if (!this.used) return; var axis = this, o = axis.options, min = o.min !== null ? o.min : axis.datamin, max = o.max !== null ? o.max : axis.datamax, margin = o.autoscaleMargin; if (o.scaling == 'logarithmic') { if (min <= 0) min = axis.datamin; // Let it widen later on if (max <= 0) max = min; } if (max == min) { var widen = max ? 0.01 : 1.00; if (o.min === null) min -= widen; if (o.max === null) max += widen; } if (o.scaling === 'logarithmic') { if (min < 0) min = max / o.base; // Could be the result of widening var maxexp = Math.log(max); if (o.base != Math.E) maxexp /= Math.log(o.base); maxexp = Math.ceil(maxexp); var minexp = Math.log(min); if (o.base != Math.E) minexp /= Math.log(o.base); minexp = Math.ceil(minexp); axis.tickSize = Flotr.getTickSize(o.noTicks, minexp, maxexp, o.tickDecimals === null ? 0 : o.tickDecimals); // Try to determine a suitable amount of miniticks based on the length of a decade if (o.minorTickFreq === null) { if (maxexp - minexp > 10) o.minorTickFreq = 0; else if (maxexp - minexp > 5) o.minorTickFreq = 2; else o.minorTickFreq = 5; } } else { axis.tickSize = Flotr.getTickSize(o.noTicks, min, max, o.tickDecimals); } axis.min = min; axis.max = max; //extendRange may use axis.min or axis.max, so it should be set before it is caled // Autoscaling. @todo This probably fails with log scale. Find a testcase and fix it if(o.min === null && o.autoscale){ axis.min -= axis.tickSize * margin; // Make sure we don't go below zero if all values are positive. if(axis.min < 0 && axis.datamin >= 0) axis.min = 0; axis.min = axis.tickSize * Math.floor(axis.min / axis.tickSize); } if(o.max === null && o.autoscale){ axis.max += axis.tickSize * margin; if(axis.max > 0 && axis.datamax <= 0 && axis.datamax != axis.datamin) axis.max = 0; axis.max = axis.tickSize * Math.ceil(axis.max / axis.tickSize); } if (axis.min == axis.max) axis.max = axis.min + 1; }, calculateTextDimensions : function (T, options) { var maxLabel = '', length, i; if (this.options.showLabels) { for (i = 0; i < this.ticks.length; ++i) { length = this.ticks[i].label.length; if (length > maxLabel.length){ maxLabel = this.ticks[i].label; } } } this.maxLabel = T.dimensions( maxLabel, {size:options.fontSize, angle: Flotr.toRad(this.options.labelsAngle)}, 'font-size:smaller;', 'flotr-grid-label' ); this.titleSize = T.dimensions( this.options.title, {size:options.fontSize*1.2, angle: Flotr.toRad(this.options.titleAngle)}, 'font-weight:bold;', 'flotr-axis-title' ); }, _cleanUserTicks : function (ticks, axisTicks) { var axis = this, options = this.options, v, i, label, tick; if(_.isFunction(ticks)) ticks = ticks({min : axis.min, max : axis.max}); for(i = 0; i < ticks.length; ++i){ tick = ticks[i]; if(typeof(tick) === 'object'){ v = tick[0]; label = (tick.length > 1) ? tick[1] : options.tickFormatter(v, {min : axis.min, max : axis.max}); } else { v = tick; label = options.tickFormatter(v, {min : this.min, max : this.max}); } axisTicks[i] = { v: v, label: label }; } }, _calculateTimeTicks : function () { this.ticks = Flotr.Date.generator(this); }, _calculateLogTicks : function () { var axis = this, o = axis.options, v, decadeStart; var max = Math.log(axis.max); if (o.base != Math.E) max /= Math.log(o.base); max = Math.ceil(max); var min = Math.log(axis.min); if (o.base != Math.E) min /= Math.log(o.base); min = Math.ceil(min); for (i = min; i < max; i += axis.tickSize) { decadeStart = (o.base == Math.E) ? Math.exp(i) : Math.pow(o.base, i); // Next decade begins here: var decadeEnd = decadeStart * ((o.base == Math.E) ? Math.exp(axis.tickSize) : Math.pow(o.base, axis.tickSize)); var stepSize = (decadeEnd - decadeStart) / o.minorTickFreq; axis.ticks.push({v: decadeStart, label: o.tickFormatter(decadeStart, {min : axis.min, max : axis.max})}); for (v = decadeStart + stepSize; v < decadeEnd; v += stepSize) axis.minorTicks.push({v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max})}); } // Always show the value at the would-be start of next decade (end of this decade) decadeStart = (o.base == Math.E) ? Math.exp(i) : Math.pow(o.base, i); axis.ticks.push({v: decadeStart, label: o.tickFormatter(decadeStart, {min : axis.min, max : axis.max})}); }, _calculateTicks : function () { var axis = this, o = axis.options, tickSize = axis.tickSize, min = axis.min, max = axis.max, start = tickSize * Math.ceil(min / tickSize), // Round to nearest multiple of tick size. decimals, minorTickSize, v, v2, i, j; if (o.minorTickFreq) minorTickSize = tickSize / o.minorTickFreq; // Then store all possible ticks. for (i = 0; (v = v2 = start + i * tickSize) <= max; ++i){ // Round (this is always needed to fix numerical instability). decimals = o.tickDecimals; if (decimals === null) decimals = 1 - Math.floor(Math.log(tickSize) / Math.LN10); if (decimals < 0) decimals = 0; v = v.toFixed(decimals); axis.ticks.push({ v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max}) }); if (o.minorTickFreq) { for (j = 0; j < o.minorTickFreq && (i * tickSize + j * minorTickSize) < max; ++j) { v = v2 + j * minorTickSize; axis.minorTicks.push({ v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max}) }); } } } } }; // Static Methods _.extend(Axis, { getAxes : function (options) { return { x: new Axis({options: options.xaxis, n: 1, length: this.plotWidth}), x2: new Axis({options: options.x2axis, n: 2, length: this.plotWidth}), y: new Axis({options: options.yaxis, n: 1, length: this.plotHeight, offset: this.plotHeight, orientation: -1}), y2: new Axis({options: options.y2axis, n: 2, length: this.plotHeight, offset: this.plotHeight, orientation: -1}) }; } }); // Helper Methods function log (value, base) { value = Math.log(Math.max(value, Number.MIN_VALUE)); if (base !== Math.E) value /= Math.log(base); return value; } function exp (value, base) { return (base === Math.E) ? Math.exp(value) : Math.pow(base, value); } Flotr.Axis = Axis; })(); /** * Flotr Series Library */ (function () { var _ = Flotr._; function Series (o) { _.extend(this, o); } Series.prototype = { getRange: function () { var data = this.data, length = data.length, xmin = Number.MAX_VALUE, ymin = Number.MAX_VALUE, xmax = -Number.MAX_VALUE, ymax = -Number.MAX_VALUE, xused = false, yused = false, x, y, i; if (length < 0 || this.hide) return false; for (i = 0; i < length; i++) { x = data[i][0]; y = data[i][1]; if (x !== null) { if (x < xmin) { xmin = x; xused = true; } if (x > xmax) { xmax = x; xused = true; } } if (y !== null) { if (y < ymin) { ymin = y; yused = true; } if (y > ymax) { ymax = y; yused = true; } } } return { xmin : xmin, xmax : xmax, ymin : ymin, ymax : ymax, xused : xused, yused : yused }; } }; _.extend(Series, { /** * Collects dataseries from input and parses the series into the right format. It returns an Array * of Objects each having at least the 'data'