(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'