Skip to content

Instantly share code, notes, and snippets.

@fiznool
Created July 3, 2012 14:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save fiznool/3040127 to your computer and use it in GitHub Desktop.
Save fiznool/3040127 to your computer and use it in GitHub Desktop.
Flotr2 AMD
(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.Flotr2 = factory(bean, _));
});
} else {
// Browser globals
root.Flotr2 = 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)
*
* Custom build: no bean / underscore
*/
(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, '<div style="'+style+'" class="'+className+' flotr-dummy-div">' + text + '</div>');
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 < xmin) { xmin = x; xused = true; }
if (x > xmax) { xmax = x; xused = true; }
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' key set.
* @param {Array, Object} data - Object or array of dataseries
* @return {Array} Array of Objects parsed into the right format ({(...,) data: [[x1,y1], [x2,y2], ...] (, ...)})
*/
getSeries: function(data){
return _.map(data, function(s){
var series;
if (s.data) {
series = new Series();
_.extend(series, s);
} else {
series = new Series({data:s});
}
return series;
});
}
});
Flotr.Series = Series;
})();
/** Lines **/
Flotr.addType('lines', {
options: {
show: false, // => setting to true will show lines, false will hide
lineWidth: 2, // => line width in pixels
fill: false, // => true to fill the area from the line to the x axis, false for (transparent) no fill
fillBorder: false, // => draw a border around the fill
fillColor: null, // => fill color
fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
steps: false, // => draw steps
stacked: false // => setting to true will show stacked lines, false will show normal lines
},
stack : {
values : []
},
/**
* Draws lines series in the canvas element.
* @param {Object} options
*/
draw : function (options) {
var
context = options.context,
lineWidth = options.lineWidth,
shadowSize = options.shadowSize,
offset;
context.save();
context.lineJoin = 'round';
if (shadowSize) {
context.lineWidth = shadowSize / 2;
offset = lineWidth / 2 + context.lineWidth / 2;
// @TODO do this instead with a linear gradient
context.strokeStyle = "rgba(0,0,0,0.1)";
this.plot(options, offset + shadowSize / 2, false);
context.strokeStyle = "rgba(0,0,0,0.2)";
this.plot(options, offset, false);
}
context.lineWidth = lineWidth;
context.strokeStyle = options.color;
this.plot(options, 0, true);
context.restore();
},
plot : function (options, shadowOffset, incStack) {
var
context = options.context,
width = options.width,
height = options.height,
xScale = options.xScale,
yScale = options.yScale,
data = options.data,
stack = options.stacked ? this.stack : false,
length = data.length - 1,
prevx = null,
prevy = null,
zero = yScale(0),
start = null,
x1, x2, y1, y2, stack1, stack2, i;
if (length < 1) return;
context.beginPath();
for (i = 0; i < length; ++i) {
// To allow empty values
if (data[i][1] === null || data[i+1][1] === null) {
if (options.fill) {
if (i > 0 && data[i][1]) {
context.stroke();
fill();
start = null;
context.closePath();
context.beginPath();
}
}
continue;
}
// Zero is infinity for log scales
// TODO handle zero for logarithmic
// if (xa.options.scaling === 'logarithmic' && (data[i][0] <= 0 || data[i+1][0] <= 0)) continue;
// if (ya.options.scaling === 'logarithmic' && (data[i][1] <= 0 || data[i+1][1] <= 0)) continue;
x1 = xScale(data[i][0]);
x2 = xScale(data[i+1][0]);
if (start === null) start = data[i];
if (stack) {
stack1 = stack.values[data[i][0]] || 0;
stack2 = stack.values[data[i+1][0]] || stack.values[data[i][0]] || 0;
y1 = yScale(data[i][1] + stack1);
y2 = yScale(data[i+1][1] + stack2);
if(incStack){
stack.values[data[i][0]] = data[i][1]+stack1;
if(i == length-1)
stack.values[data[i+1][0]] = data[i+1][1]+stack2;
}
}
else{
y1 = yScale(data[i][1]);
y2 = yScale(data[i+1][1]);
}
if (
(y1 > height && y2 > height) ||
(y1 < 0 && y2 < 0) ||
(x1 < 0 && x2 < 0) ||
(x1 > width && x2 > width)
) continue;
if((prevx != x1) || (prevy != y1 + shadowOffset))
context.moveTo(x1, y1 + shadowOffset);
prevx = x2;
prevy = y2 + shadowOffset;
if (options.steps) {
context.lineTo(prevx + shadowOffset / 2, y1 + shadowOffset);
context.lineTo(prevx + shadowOffset / 2, prevy);
} else {
context.lineTo(prevx, prevy);
}
}
if (!options.fill || options.fill && !options.fillBorder) context.stroke();
fill();
function fill () {
// TODO stacked lines
if(!shadowOffset && options.fill && start){
x1 = xScale(start[0]);
context.fillStyle = options.fillStyle;
context.lineTo(x2, zero);
context.lineTo(x1, zero);
context.lineTo(x1, yScale(start[1]));
context.fill();
if (options.fillBorder) {
context.stroke();
}
}
}
context.closePath();
},
// Perform any pre-render precalculations (this should be run on data first)
// - Pie chart total for calculating measures
// - Stacks for lines and bars
// precalculate : function () {
// }
//
//
// Get any bounds after pre calculation (axis can fetch this if does not have explicit min/max)
// getBounds : function () {
// }
// getMin : function () {
// }
// getMax : function () {
// }
//
//
// Padding around rendered elements
// getPadding : function () {
// }
extendYRange : function (axis, data, options, lines) {
var o = axis.options;
// If stacked and auto-min
if (options.stacked && ((!o.max && o.max !== 0) || (!o.min && o.min !== 0))) {
var
newmax = axis.max,
newmin = axis.min,
positiveSums = lines.positiveSums || {},
negativeSums = lines.negativeSums || {},
x, j;
for (j = 0; j < data.length; j++) {
x = data[j][0] + '';
// Positive
if (data[j][1] > 0) {
positiveSums[x] = (positiveSums[x] || 0) + data[j][1];
newmax = Math.max(newmax, positiveSums[x]);
}
// Negative
else {
negativeSums[x] = (negativeSums[x] || 0) + data[j][1];
newmin = Math.min(newmin, negativeSums[x]);
}
}
lines.negativeSums = negativeSums;
lines.positiveSums = positiveSums;
axis.max = newmax;
axis.min = newmin;
}
if (options.steps) {
this.hit = function (options) {
var
data = options.data,
args = options.args,
yScale = options.yScale,
mouse = args[0],
length = data.length,
n = args[1],
x = options.xInverse(mouse.relX),
relY = mouse.relY,
i;
for (i = 0; i < length - 1; i++) {
if (x >= data[i][0] && x <= data[i+1][0]) {
if (Math.abs(yScale(data[i][1]) - relY) < 8) {
n.x = data[i][0];
n.y = data[i][1];
n.index = i;
n.seriesIndex = options.index;
}
break;
}
}
};
this.drawHit = function (options) {
var
context = options.context,
args = options.args,
data = options.data,
xScale = options.xScale,
index = args.index,
x = xScale(args.x),
y = options.yScale(args.y),
x2;
if (data.length - 1 > index) {
x2 = options.xScale(data[index + 1][0]);
context.save();
context.strokeStyle = options.color;
context.lineWidth = options.lineWidth;
context.beginPath();
context.moveTo(x, y);
context.lineTo(x2, y);
context.stroke();
context.closePath();
context.restore();
}
};
this.clearHit = function (options) {
var
context = options.context,
args = options.args,
data = options.data,
xScale = options.xScale,
width = options.lineWidth,
index = args.index,
x = xScale(args.x),
y = options.yScale(args.y),
x2;
if (data.length - 1 > index) {
x2 = options.xScale(data[index + 1][0]);
context.clearRect(x - width, y - width, x2 - x + 2 * width, 2 * width);
}
};
}
}
});
/** Bars **/
Flotr.addType('bars', {
options: {
show: false, // => setting to true will show bars, false will hide
lineWidth: 2, // => in pixels
barWidth: 1, // => in units of the x axis
fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill
fillColor: null, // => fill color
fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
horizontal: false, // => horizontal bars (x and y inverted)
stacked: false, // => stacked bar charts
centered: true, // => center the bars to their x axis value
topPadding: 0.1, // => top padding in percent
grouped: false // => groups bars together which share x value, hit not supported.
},
stack : {
positive : [],
negative : [],
_positive : [], // Shadow
_negative : [] // Shadow
},
draw : function (options) {
var
context = options.context;
this.current += 1;
context.save();
context.lineJoin = 'miter';
// @TODO linewidth not interpreted the right way.
context.lineWidth = options.lineWidth;
context.strokeStyle = options.color;
if (options.fill) context.fillStyle = options.fillStyle;
this.plot(options);
context.restore();
},
plot : function (options) {
var
data = options.data,
context = options.context,
shadowSize = options.shadowSize,
i, geometry, left, top, width, height;
if (data.length < 1) return;
this.translate(context, options.horizontal);
for (i = 0; i < data.length; i++) {
geometry = this.getBarGeometry(data[i][0], data[i][1], options);
if (geometry === null) continue;
left = geometry.left;
top = geometry.top;
width = geometry.width;
height = geometry.height;
if (options.fill) context.fillRect(left, top, width, height);
if (shadowSize) {
context.save();
context.fillStyle = 'rgba(0,0,0,0.05)';
context.fillRect(left + shadowSize, top + shadowSize, width, height);
context.restore();
}
if (options.lineWidth) {
context.strokeRect(left, top, width, height);
}
}
},
translate : function (context, horizontal) {
if (horizontal) {
context.rotate(-Math.PI / 2);
context.scale(-1, 1);
}
},
getBarGeometry : function (x, y, options) {
var
horizontal = options.horizontal,
barWidth = options.barWidth,
centered = options.centered,
stack = options.stacked ? this.stack : false,
lineWidth = options.lineWidth,
bisection = centered ? barWidth / 2 : 0,
xScale = horizontal ? options.yScale : options.xScale,
yScale = horizontal ? options.xScale : options.yScale,
xValue = horizontal ? y : x,
yValue = horizontal ? x : y,
stackOffset = 0,
stackValue, left, right, top, bottom;
if (options.grouped) {
this.current / this.groups;
xValue = xValue - bisection;
barWidth = barWidth / this.groups;
bisection = barWidth / 2;
xValue = xValue + barWidth * this.current - bisection;
}
// Stacked bars
if (stack) {
stackValue = yValue > 0 ? stack.positive : stack.negative;
stackOffset = stackValue[xValue] || stackOffset;
stackValue[xValue] = stackOffset + yValue;
}
left = xScale(xValue - bisection);
right = xScale(xValue + barWidth - bisection);
top = yScale(yValue + stackOffset);
bottom = yScale(stackOffset);
// TODO for test passing... probably looks better without this
if (bottom < 0) bottom = 0;
// TODO Skipping...
// if (right < xa.min || left > xa.max || top < ya.min || bottom > ya.max) continue;
return (x === null || y === null) ? null : {
x : xValue,
y : yValue,
xScale : xScale,
yScale : yScale,
top : top,
left : Math.min(left, right) - lineWidth / 2,
width : Math.abs(right - left) - lineWidth,
height : bottom - top
};
},
hit : function (options) {
var
data = options.data,
args = options.args,
mouse = args[0],
n = args[1],
x = options.xInverse(mouse.relX),
y = options.yInverse(mouse.relY),
hitGeometry = this.getBarGeometry(x, y, options),
width = hitGeometry.width / 2,
left = hitGeometry.left,
height = hitGeometry.y,
geometry, i;
for (i = data.length; i--;) {
geometry = this.getBarGeometry(data[i][0], data[i][1], options);
if (
// Height:
(
// Positive Bars:
(height > 0 && height < geometry.y) ||
// Negative Bars:
(height < 0 && height > geometry.y)
) &&
// Width:
(Math.abs(left - geometry.left) < width)
) {
n.x = data[i][0];
n.y = data[i][1];
n.index = i;
n.seriesIndex = options.index;
}
}
},
drawHit : function (options) {
// TODO hits for stacked bars; implement using calculateStack option?
var
context = options.context,
args = options.args,
geometry = this.getBarGeometry(args.x, args.y, options),
left = geometry.left,
top = geometry.top,
width = geometry.width,
height = geometry.height;
context.save();
context.strokeStyle = options.color;
context.lineWidth = options.lineWidth;
this.translate(context, options.horizontal);
// Draw highlight
context.beginPath();
context.moveTo(left, top + height);
context.lineTo(left, top);
context.lineTo(left + width, top);
context.lineTo(left + width, top + height);
if (options.fill) {
context.fillStyle = options.fillStyle;
context.fill();
}
context.stroke();
context.closePath();
context.restore();
},
clearHit: function (options) {
var
context = options.context,
args = options.args,
geometry = this.getBarGeometry(args.x, args.y, options),
left = geometry.left,
width = geometry.width,
top = geometry.top,
height = geometry.height,
lineWidth = 2 * options.lineWidth;
context.save();
this.translate(context, options.horizontal);
context.clearRect(
left - lineWidth,
Math.min(top, top + height) - lineWidth,
width + 2 * lineWidth,
Math.abs(height) + 2 * lineWidth
);
context.restore();
},
extendXRange : function (axis, data, options, bars) {
this._extendRange(axis, data, options, bars);
this.groups = (this.groups + 1) || 1;
this.current = 0;
},
extendYRange : function (axis, data, options, bars) {
this._extendRange(axis, data, options, bars);
},
_extendRange: function (axis, data, options, bars) {
var
max = axis.options.max;
if (_.isNumber(max) || _.isString(max)) return;
var
newmin = axis.min,
newmax = axis.max,
horizontal = options.horizontal,
orientation = axis.orientation,
positiveSums = this.positiveSums || {},
negativeSums = this.negativeSums || {},
value, datum, index, j;
// Sides of bars
if ((orientation == 1 && !horizontal) || (orientation == -1 && horizontal)) {
if (options.centered) {
newmax = Math.max(axis.datamax + options.barWidth, newmax);
newmin = Math.min(axis.datamin - options.barWidth, newmin);
}
}
if (options.stacked &&
((orientation == 1 && horizontal) || (orientation == -1 && !horizontal))){
for (j = data.length; j--;) {
value = data[j][(orientation == 1 ? 1 : 0)]+'';
datum = data[j][(orientation == 1 ? 0 : 1)];
// Positive
if (datum > 0) {
positiveSums[value] = (positiveSums[value] || 0) + datum;
newmax = Math.max(newmax, positiveSums[value]);
}
// Negative
else {
negativeSums[value] = (negativeSums[value] || 0) + datum;
newmin = Math.min(newmin, negativeSums[value]);
}
}
}
// End of bars
if ((orientation == 1 && horizontal) || (orientation == -1 && !horizontal)) {
if (options.topPadding && (axis.max === axis.datamax || (options.stacked && this.stackMax !== newmax))) {
newmax += options.topPadding * (newmax - newmin);
}
}
this.stackMin = newmin;
this.stackMax = newmax;
this.negativeSums = negativeSums;
this.positiveSums = positiveSums;
axis.max = newmax;
axis.min = newmin;
}
});
/** Bubbles **/
Flotr.addType('bubbles', {
options: {
show: false, // => setting to true will show radar chart, false will hide
lineWidth: 2, // => line width in pixels
fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill
fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
baseRadius: 2 // => ratio of the radar, against the plot size
},
draw : function (options) {
var
context = options.context,
shadowSize = options.shadowSize;
context.save();
context.lineWidth = options.lineWidth;
// Shadows
context.fillStyle = 'rgba(0,0,0,0.05)';
context.strokeStyle = 'rgba(0,0,0,0.05)';
this.plot(options, shadowSize / 2);
context.strokeStyle = 'rgba(0,0,0,0.1)';
this.plot(options, shadowSize / 4);
// Chart
context.strokeStyle = options.color;
context.fillStyle = options.fillStyle;
this.plot(options);
context.restore();
},
plot : function (options, offset) {
var
data = options.data,
context = options.context,
geometry,
i, x, y, z;
offset = offset || 0;
for (i = 0; i < data.length; ++i){
geometry = this.getGeometry(data[i], options);
context.beginPath();
context.arc(geometry.x + offset, geometry.y + offset, geometry.z, 0, 2 * Math.PI, true);
context.stroke();
if (options.fill) context.fill();
context.closePath();
}
},
getGeometry : function (point, options) {
return {
x : options.xScale(point[0]),
y : options.yScale(point[1]),
z : point[2] * options.baseRadius
};
},
hit : function (options) {
var
data = options.data,
args = options.args,
mouse = args[0],
n = args[1],
relX = mouse.relX,
relY = mouse.relY,
distance,
geometry,
dx, dy;
n.best = n.best || Number.MAX_VALUE;
for (i = data.length; i--;) {
geometry = this.getGeometry(data[i], options);
dx = geometry.x - relX;
dy = geometry.y - relY;
distance = Math.sqrt(dx * dx + dy * dy);
if (distance < geometry.z && geometry.z < n.best) {
n.x = data[i][0];
n.y = data[i][1];
n.index = i;
n.seriesIndex = options.index;
n.best = geometry.z;
}
}
},
drawHit : function (options) {
var
context = options.context,
geometry = this.getGeometry(options.data[options.args.index], options);
context.save();
context.lineWidth = options.lineWidth;
context.fillStyle = options.fillStyle;
context.strokeStyle = options.color;
context.beginPath();
context.arc(geometry.x, geometry.y, geometry.z, 0, 2 * Math.PI, true);
context.fill();
context.stroke();
context.closePath();
context.restore();
},
clearHit : function (options) {
var
context = options.context,
geometry = this.getGeometry(options.data[options.args.index], options),
offset = geometry.z + options.lineWidth;
context.save();
context.clearRect(
geometry.x - offset,
geometry.y - offset,
2 * offset,
2 * offset
);
context.restore();
}
// TODO Add a hit calculation method (like pie)
});
/** Candles **/
Flotr.addType('candles', {
options: {
show: false, // => setting to true will show candle sticks, false will hide
lineWidth: 1, // => in pixels
wickLineWidth: 1, // => in pixels
candleWidth: 0.6, // => in units of the x axis
fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill
upFillColor: '#00A8F0',// => up sticks fill color
downFillColor: '#CB4B4B',// => down sticks fill color
fillOpacity: 0.5, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
// TODO Test this barcharts option.
barcharts: false // => draw as barcharts (not standard bars but financial barcharts)
},
draw : function (options) {
var
context = options.context;
context.save();
context.lineJoin = 'miter';
context.lineCap = 'butt';
// @TODO linewidth not interpreted the right way.
context.lineWidth = options.wickLineWidth || options.lineWidth;
this.plot(options);
context.restore();
},
plot : function (options) {
var
data = options.data,
context = options.context,
xScale = options.xScale,
yScale = options.yScale,
width = options.candleWidth / 2,
shadowSize = options.shadowSize,
lineWidth = options.lineWidth,
wickLineWidth = options.wickLineWidth,
pixelOffset = (wickLineWidth % 2) / 2,
color,
datum, x, y,
open, high, low, close,
left, right, bottom, top, bottom2, top2,
i;
if (data.length < 1) return;
for (i = 0; i < data.length; i++) {
datum = data[i];
x = datum[0];
open = datum[1];
high = datum[2];
low = datum[3];
close = datum[4];
left = xScale(x - width);
right = xScale(x + width);
bottom = yScale(low);
top = yScale(high);
bottom2 = yScale(Math.min(open, close));
top2 = yScale(Math.max(open, close));
/*
// TODO skipping
if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max)
continue;
*/
color = options[open > close ? 'downFillColor' : 'upFillColor'];
// Fill the candle.
// TODO Test the barcharts option
if (options.fill && !options.barcharts) {
context.fillStyle = 'rgba(0,0,0,0.05)';
context.fillRect(left + shadowSize, top2 + shadowSize, right - left, bottom2 - top2);
context.save();
context.globalAlpha = options.fillOpacity;
context.fillStyle = color;
context.fillRect(left, top2 + lineWidth, right - left, bottom2 - top2);
context.restore();
}
// Draw candle outline/border, high, low.
if (lineWidth || wickLineWidth) {
x = Math.floor((left + right) / 2) + pixelOffset;
context.strokeStyle = color;
context.beginPath();
// TODO Again with the bartcharts
if (options.barcharts) {
context.moveTo(x, Math.floor(top + width));
context.lineTo(x, Math.floor(bottom + width));
y = Math.floor(open + width) + 0.5;
context.moveTo(Math.floor(left) + pixelOffset, y);
context.lineTo(x, y);
y = Math.floor(close + width) + 0.5;
context.moveTo(Math.floor(right) + pixelOffset, y);
context.lineTo(x, y);
} else {
context.strokeRect(left, top2 + lineWidth, right - left, bottom2 - top2);
context.moveTo(x, Math.floor(top2 + lineWidth));
context.lineTo(x, Math.floor(top + lineWidth));
context.moveTo(x, Math.floor(bottom2 + lineWidth));
context.lineTo(x, Math.floor(bottom + lineWidth));
}
context.closePath();
context.stroke();
}
}
},
extendXRange: function (axis, data, options) {
if (axis.options.max === null) {
axis.max = Math.max(axis.datamax + 0.5, axis.max);
axis.min = Math.min(axis.datamin - 0.5, axis.min);
}
}
});
/** Gantt
* Base on data in form [s,y,d] where:
* y - executor or simply y value
* s - task start value
* d - task duration
* **/
Flotr.addType('gantt', {
options: {
show: false, // => setting to true will show gantt, false will hide
lineWidth: 2, // => in pixels
barWidth: 1, // => in units of the x axis
fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill
fillColor: null, // => fill color
fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
centered: true // => center the bars to their x axis value
},
/**
* Draws gantt series in the canvas element.
* @param {Object} series - Series with options.gantt.show = true.
*/
draw: function(series) {
var ctx = this.ctx,
bw = series.gantt.barWidth,
lw = Math.min(series.gantt.lineWidth, bw);
ctx.save();
ctx.translate(this.plotOffset.left, this.plotOffset.top);
ctx.lineJoin = 'miter';
/**
* @todo linewidth not interpreted the right way.
*/
ctx.lineWidth = lw;
ctx.strokeStyle = series.color;
ctx.save();
this.gantt.plotShadows(series, bw, 0, series.gantt.fill);
ctx.restore();
if(series.gantt.fill){
var color = series.gantt.fillColor || series.color;
ctx.fillStyle = this.processColor(color, {opacity: series.gantt.fillOpacity});
}
this.gantt.plot(series, bw, 0, series.gantt.fill);
ctx.restore();
},
plot: function(series, barWidth, offset, fill){
var data = series.data;
if(data.length < 1) return;
var xa = series.xaxis,
ya = series.yaxis,
ctx = this.ctx, i;
for(i = 0; i < data.length; i++){
var y = data[i][0],
s = data[i][1],
d = data[i][2],
drawLeft = true, drawTop = true, drawRight = true;
if (s === null || d === null) continue;
var left = s,
right = s + d,
bottom = y - (series.gantt.centered ? barWidth/2 : 0),
top = y + barWidth - (series.gantt.centered ? barWidth/2 : 0);
if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max)
continue;
if(left < xa.min){
left = xa.min;
drawLeft = false;
}
if(right > xa.max){
right = xa.max;
if (xa.lastSerie != series)
drawTop = false;
}
if(bottom < ya.min)
bottom = ya.min;
if(top > ya.max){
top = ya.max;
if (ya.lastSerie != series)
drawTop = false;
}
/**
* Fill the bar.
*/
if(fill){
ctx.beginPath();
ctx.moveTo(xa.d2p(left), ya.d2p(bottom) + offset);
ctx.lineTo(xa.d2p(left), ya.d2p(top) + offset);
ctx.lineTo(xa.d2p(right), ya.d2p(top) + offset);
ctx.lineTo(xa.d2p(right), ya.d2p(bottom) + offset);
ctx.fill();
ctx.closePath();
}
/**
* Draw bar outline/border.
*/
if(series.gantt.lineWidth && (drawLeft || drawRight || drawTop)){
ctx.beginPath();
ctx.moveTo(xa.d2p(left), ya.d2p(bottom) + offset);
ctx[drawLeft ?'lineTo':'moveTo'](xa.d2p(left), ya.d2p(top) + offset);
ctx[drawTop ?'lineTo':'moveTo'](xa.d2p(right), ya.d2p(top) + offset);
ctx[drawRight?'lineTo':'moveTo'](xa.d2p(right), ya.d2p(bottom) + offset);
ctx.stroke();
ctx.closePath();
}
}
},
plotShadows: function(series, barWidth, offset){
var data = series.data;
if(data.length < 1) return;
var i, y, s, d,
xa = series.xaxis,
ya = series.yaxis,
ctx = this.ctx,
sw = this.options.shadowSize;
for(i = 0; i < data.length; i++){
y = data[i][0];
s = data[i][1];
d = data[i][2];
if (s === null || d === null) continue;
var left = s,
right = s + d,
bottom = y - (series.gantt.centered ? barWidth/2 : 0),
top = y + barWidth - (series.gantt.centered ? barWidth/2 : 0);
if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max)
continue;
if(left < xa.min) left = xa.min;
if(right > xa.max) right = xa.max;
if(bottom < ya.min) bottom = ya.min;
if(top > ya.max) top = ya.max;
var width = xa.d2p(right)-xa.d2p(left)-((xa.d2p(right)+sw <= this.plotWidth) ? 0 : sw);
var height = ya.d2p(bottom)-ya.d2p(top)-((ya.d2p(bottom)+sw <= this.plotHeight) ? 0 : sw );
ctx.fillStyle = 'rgba(0,0,0,0.05)';
ctx.fillRect(Math.min(xa.d2p(left)+sw, this.plotWidth), Math.min(ya.d2p(top)+sw, this.plotHeight), width, height);
}
},
extendXRange: function(axis) {
if(axis.options.max === null){
var newmin = axis.min,
newmax = axis.max,
i, j, x, s, g,
stackedSumsPos = {},
stackedSumsNeg = {},
lastSerie = null;
for(i = 0; i < this.series.length; ++i){
s = this.series[i];
g = s.gantt;
if(g.show && s.xaxis == axis) {
for (j = 0; j < s.data.length; j++) {
if (g.show) {
y = s.data[j][0]+'';
stackedSumsPos[y] = Math.max((stackedSumsPos[y] || 0), s.data[j][1]+s.data[j][2]);
lastSerie = s;
}
}
for (j in stackedSumsPos) {
newmax = Math.max(stackedSumsPos[j], newmax);
}
}
}
axis.lastSerie = lastSerie;
axis.max = newmax;
axis.min = newmin;
}
},
extendYRange: function(axis){
if(axis.options.max === null){
var newmax = Number.MIN_VALUE,
newmin = Number.MAX_VALUE,
i, j, s, g,
stackedSumsPos = {},
stackedSumsNeg = {},
lastSerie = null;
for(i = 0; i < this.series.length; ++i){
s = this.series[i];
g = s.gantt;
if (g.show && !s.hide && s.yaxis == axis) {
var datamax = Number.MIN_VALUE, datamin = Number.MAX_VALUE;
for(j=0; j < s.data.length; j++){
datamax = Math.max(datamax,s.data[j][0]);
datamin = Math.min(datamin,s.data[j][0]);
}
if (g.centered) {
newmax = Math.max(datamax + 0.5, newmax);
newmin = Math.min(datamin - 0.5, newmin);
}
else {
newmax = Math.max(datamax + 1, newmax);
newmin = Math.min(datamin, newmin);
}
// For normal horizontal bars
if (g.barWidth + datamax > newmax){
newmax = axis.max + g.barWidth;
}
}
}
axis.lastSerie = lastSerie;
axis.max = newmax;
axis.min = newmin;
axis.tickSize = Flotr.getTickSize(axis.options.noTicks, newmin, newmax, axis.options.tickDecimals);
}
}
});
/** Markers **/
/**
* Formats the marker labels.
* @param {Object} obj - Marker value Object {x:..,y:..}
* @return {String} Formatted marker string
*/
(function () {
Flotr.defaultMarkerFormatter = function(obj){
return (Math.round(obj.y*100)/100)+'';
};
Flotr.addType('markers', {
options: {
show: false, // => setting to true will show markers, false will hide
lineWidth: 1, // => line width of the rectangle around the marker
color: '#000000', // => text color
fill: false, // => fill or not the marekers' rectangles
fillColor: "#FFFFFF", // => fill color
fillOpacity: 0.4, // => fill opacity
stroke: false, // => draw the rectangle around the markers
position: 'ct', // => the markers position (vertical align: b, m, t, horizontal align: l, c, r)
verticalMargin: 0, // => the margin between the point and the text.
labelFormatter: Flotr.defaultMarkerFormatter,
fontSize: Flotr.defaultOptions.fontSize,
stacked: false, // => true if markers should be stacked
stackingType: 'b', // => define staching behavior, (b- bars like, a - area like) (see Issue 125 for details)
horizontal: false // => true if markers should be horizontal (For now only in a case on horizontal stacked bars, stacks should be calculated horizontaly)
},
// TODO test stacked markers.
stack : {
positive : [],
negative : [],
values : []
},
draw : function (options) {
var
data = options.data,
context = options.context,
stack = options.stacked ? options.stack : false,
stackType = options.stackingType,
stackOffsetNeg,
stackOffsetPos,
stackOffset,
i, x, y, label;
context.save();
context.lineJoin = 'round';
context.lineWidth = options.lineWidth;
context.strokeStyle = 'rgba(0,0,0,0.5)';
context.fillStyle = options.fillStyle;
function stackPos (a, b) {
stackOffsetPos = stack.negative[a] || 0;
stackOffsetNeg = stack.positive[a] || 0;
if (b > 0) {
stack.positive[a] = stackOffsetPos + b;
return stackOffsetPos + b;
} else {
stack.negative[a] = stackOffsetNeg + b;
return stackOffsetNeg + b;
}
}
for (i = 0; i < data.length; ++i) {
x = data[i][0];
y = data[i][1];
if (stack) {
if (stackType == 'b') {
if (options.horizontal) y = stackPos(y, x);
else x = stackPos(x, y);
} else if (stackType == 'a') {
stackOffset = stack.values[x] || 0;
stack.values[x] = stackOffset + y;
y = stackOffset + y;
}
}
label = options.labelFormatter({x: x, y: y, index: i, data : data});
this.plot(options.xScale(x), options.yScale(y), label, options);
}
context.restore();
},
plot: function(x, y, label, options) {
var context = options.context;
if (isImage(label) && !label.complete) {
throw 'Marker image not loaded.';
} else {
this._plot(x, y, label, options);
}
},
_plot: function(x, y, label, options) {
var context = options.context,
margin = 2,
left = x,
top = y,
dim;
if (isImage(label))
dim = {height : label.height, width: label.width};
else
dim = options.text.canvas(label);
dim.width = Math.floor(dim.width+margin*2);
dim.height = Math.floor(dim.height+margin*2);
if (options.position.indexOf('c') != -1) left -= dim.width/2 + margin;
else if (options.position.indexOf('l') != -1) left -= dim.width;
if (options.position.indexOf('m') != -1) top -= dim.height/2 + margin;
else if (options.position.indexOf('t') != -1) top -= dim.height + options.verticalMargin;
else top += options.verticalMargin;
left = Math.floor(left)+0.5;
top = Math.floor(top)+0.5;
if(options.fill)
context.fillRect(left, top, dim.width, dim.height);
if(options.stroke)
context.strokeRect(left, top, dim.width, dim.height);
if (isImage(label))
context.drawImage(label, left+margin, top+margin);
else
Flotr.drawText(context, label, left+margin, top+margin, {textBaseline: 'top', textAlign: 'left', size: options.fontSize, color: options.color});
}
});
function isImage (i) {
return typeof i === 'object' && i.constructor && (Image ? true : i.constructor === Image);
}
})();
/**
* Pie
*
* Formats the pies labels.
* @param {Object} slice - Slice object
* @return {String} Formatted pie label string
*/
(function () {
var
_ = Flotr._;
Flotr.defaultPieLabelFormatter = function (total, value) {
return (100 * value / total).toFixed(2)+'%';
};
Flotr.addType('pie', {
options: {
show: false, // => setting to true will show bars, false will hide
lineWidth: 1, // => in pixels
fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill
fillColor: null, // => fill color
fillOpacity: 0.6, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
explode: 6, // => the number of pixels the splices will be far from the center
sizeRatio: 0.6, // => the size ratio of the pie relative to the plot
startAngle: Math.PI/4, // => the first slice start angle
labelFormatter: Flotr.defaultPieLabelFormatter,
pie3D: false, // => whether to draw the pie in 3 dimenstions or not (ineffective)
pie3DviewAngle: (Math.PI/2 * 0.8),
pie3DspliceThickness: 20,
epsilon: 0.1 // => how close do you have to get to hit empty slice
},
draw : function (options) {
// TODO 3D charts what?
var
data = options.data,
context = options.context,
canvas = context.canvas,
lineWidth = options.lineWidth,
shadowSize = options.shadowSize,
sizeRatio = options.sizeRatio,
height = options.height,
width = options.width,
explode = options.explode,
color = options.color,
fill = options.fill,
fillStyle = options.fillStyle,
radius = Math.min(canvas.width, canvas.height) * sizeRatio / 2,
value = data[0][1],
html = [],
vScale = 1,//Math.cos(series.pie.viewAngle);
measure = Math.PI * 2 * value / this.total,
startAngle = this.startAngle || (2 * Math.PI * options.startAngle), // TODO: this initial startAngle is already in radians (fixing will be test-unstable)
endAngle = startAngle + measure,
bisection = startAngle + measure / 2,
label = options.labelFormatter(this.total, value),
//plotTickness = Math.sin(series.pie.viewAngle)*series.pie.spliceThickness / vScale;
explodeCoeff = explode + radius + 4,
distX = Math.cos(bisection) * explodeCoeff,
distY = Math.sin(bisection) * explodeCoeff,
textAlign = distX < 0 ? 'right' : 'left',
textBaseline = distY > 0 ? 'top' : 'bottom',
style,
x, y;
context.save();
context.translate(width / 2, height / 2);
context.scale(1, vScale);
x = Math.cos(bisection) * explode;
y = Math.sin(bisection) * explode;
// Shadows
if (shadowSize > 0) {
this.plotSlice(x + shadowSize, y + shadowSize, radius, startAngle, endAngle, context);
if (fill) {
context.fillStyle = 'rgba(0,0,0,0.1)';
context.fill();
}
}
this.plotSlice(x, y, radius, startAngle, endAngle, context);
if (fill) {
context.fillStyle = fillStyle;
context.fill();
}
context.lineWidth = lineWidth;
context.strokeStyle = color;
context.stroke();
style = {
size : options.fontSize * 1.2,
color : options.fontColor,
weight : 1.5
};
if (label) {
if (options.htmlText || !options.textEnabled) {
divStyle = 'position:absolute;' + textBaseline + ':' + (height / 2 + (textBaseline === 'top' ? distY : -distY)) + 'px;';
divStyle += textAlign + ':' + (width / 2 + (textAlign === 'right' ? -distX : distX)) + 'px;';
html.push('<div style="', divStyle, '" class="flotr-grid-label">', label, '</div>');
}
else {
style.textAlign = textAlign;
style.textBaseline = textBaseline;
Flotr.drawText(context, label, distX, distY, style);
}
}
if (options.htmlText || !options.textEnabled) {
var div = Flotr.DOM.node('<div style="color:' + options.fontColor + '" class="flotr-labels"></div>');
Flotr.DOM.insert(div, html.join(''));
Flotr.DOM.insert(options.element, div);
}
context.restore();
// New start angle
this.startAngle = endAngle;
this.slices = this.slices || [];
this.slices.push({
radius : Math.min(canvas.width, canvas.height) * sizeRatio / 2,
x : x,
y : y,
explode : explode,
start : startAngle,
end : endAngle
});
},
plotSlice : function (x, y, radius, startAngle, endAngle, context) {
context.beginPath();
context.moveTo(x, y);
context.arc(x, y, radius, startAngle, endAngle, false);
context.lineTo(x, y);
context.closePath();
},
hit : function (options) {
var
data = options.data[0],
args = options.args,
index = options.index,
mouse = args[0],
n = args[1],
slice = this.slices[index],
x = mouse.relX - options.width / 2,
y = mouse.relY - options.height / 2,
r = Math.sqrt(x * x + y * y),
theta = Math.atan(y / x),
circle = Math.PI * 2,
explode = slice.explode || options.explode,
start = slice.start % circle,
end = slice.end % circle,
epsilon = options.epsilon;
if (x < 0) {
theta += Math.PI;
} else if (x > 0 && y < 0) {
theta += circle;
}
if (r < slice.radius + explode && r > explode) {
if (
(theta > start && theta < end) || // Normal Slice
(start > end && (theta < end || theta > start)) || // First slice
// TODO: Document the two cases at the end:
(start === end && ((slice.start === slice.end && Math.abs(theta - start) < epsilon) || (slice.start !== slice.end && Math.abs(theta-start) > epsilon)))
) {
// TODO Decouple this from hit plugin (chart shouldn't know what n means)
n.x = data[0];
n.y = data[1];
n.sAngle = start;
n.eAngle = end;
n.index = 0;
n.seriesIndex = index;
n.fraction = data[1] / this.total;
}
}
},
drawHit: function (options) {
var
context = options.context,
slice = this.slices[options.args.seriesIndex];
context.save();
context.translate(options.width / 2, options.height / 2);
this.plotSlice(slice.x, slice.y, slice.radius, slice.start, slice.end, context);
context.stroke();
context.restore();
},
clearHit : function (options) {
var
context = options.context,
slice = this.slices[options.args.seriesIndex],
padding = 2 * options.lineWidth,
radius = slice.radius + padding;
context.save();
context.translate(options.width / 2, options.height / 2);
context.clearRect(
slice.x - radius,
slice.y - radius,
2 * radius + padding,
2 * radius + padding
);
context.restore();
},
extendYRange : function (axis, data) {
this.total = (this.total || 0) + data[0][1];
}
});
})();
/** Points **/
Flotr.addType('points', {
options: {
show: false, // => setting to true will show points, false will hide
radius: 3, // => point radius (pixels)
lineWidth: 2, // => line width in pixels
fill: true, // => true to fill the points with a color, false for (transparent) no fill
fillColor: '#FFFFFF', // => fill color. Null to use series color.
fillOpacity: 1, // => opacity of color inside the points
hitRadius: null // => override for points hit radius
},
draw : function (options) {
var
context = options.context,
lineWidth = options.lineWidth,
shadowSize = options.shadowSize;
context.save();
if (shadowSize > 0) {
context.lineWidth = shadowSize / 2;
context.strokeStyle = 'rgba(0,0,0,0.1)';
this.plot(options, shadowSize / 2 + context.lineWidth / 2);
context.strokeStyle = 'rgba(0,0,0,0.2)';
this.plot(options, context.lineWidth / 2);
}
context.lineWidth = options.lineWidth;
context.strokeStyle = options.color;
if (options.fill) context.fillStyle = options.fillStyle;
this.plot(options);
context.restore();
},
plot : function (options, offset) {
var
data = options.data,
context = options.context,
xScale = options.xScale,
yScale = options.yScale,
i, x, y;
for (i = data.length - 1; i > -1; --i) {
y = data[i][1];
if (y === null) continue;
x = xScale(data[i][0]);
y = yScale(y);
if (x < 0 || x > options.width || y < 0 || y > options.height) continue;
context.beginPath();
if (offset) {
context.arc(x, y + offset, options.radius, 0, Math.PI, false);
} else {
context.arc(x, y, options.radius, 0, 2 * Math.PI, true);
if (options.fill) context.fill();
}
context.stroke();
context.closePath();
}
}
});
/** Radar **/
Flotr.addType('radar', {
options: {
show: false, // => setting to true will show radar chart, false will hide
lineWidth: 2, // => line width in pixels
fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill
fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
radiusRatio: 0.90 // => ratio of the radar, against the plot size
},
draw : function (options) {
var
context = options.context,
shadowSize = options.shadowSize;
context.save();
context.translate(options.width / 2, options.height / 2);
context.lineWidth = options.lineWidth;
// Shadow
context.fillStyle = 'rgba(0,0,0,0.05)';
context.strokeStyle = 'rgba(0,0,0,0.05)';
this.plot(options, shadowSize / 2);
context.strokeStyle = 'rgba(0,0,0,0.1)';
this.plot(options, shadowSize / 4);
// Chart
context.strokeStyle = options.color;
context.fillStyle = options.fillStyle;
this.plot(options);
context.restore();
},
plot : function (options, offset) {
var
data = options.data,
context = options.context,
radius = Math.min(options.height, options.width) * options.radiusRatio / 2,
step = 2 * Math.PI / data.length,
angle = -Math.PI / 2,
i, ratio;
offset = offset || 0;
context.beginPath();
for (i = 0; i < data.length; ++i) {
ratio = data[i][1] / this.max;
context[i === 0 ? 'moveTo' : 'lineTo'](
Math.cos(i * step + angle) * radius * ratio + offset,
Math.sin(i * step + angle) * radius * ratio + offset
);
}
context.closePath();
if (options.fill) context.fill();
context.stroke();
},
extendYRange : function (axis, data) {
this.max = Math.max(axis.max, this.max || -Number.MAX_VALUE);
}
});
Flotr.addType('timeline', {
options: {
show: false,
lineWidth: 1,
barWidth: 0.2,
fill: true,
fillColor: null,
fillOpacity: 0.4,
centered: true
},
draw : function (options) {
var
context = options.context;
context.save();
context.lineJoin = 'miter';
context.lineWidth = options.lineWidth;
context.strokeStyle = options.color;
context.fillStyle = options.fillStyle;
this.plot(options);
context.restore();
},
plot : function (options) {
var
data = options.data,
context = options.context,
xScale = options.xScale,
yScale = options.yScale,
barWidth = options.barWidth,
lineWidth = options.lineWidth,
i;
Flotr._.each(data, function (timeline) {
var
x = timeline[0],
y = timeline[1],
w = timeline[2],
h = barWidth,
xt = Math.ceil(xScale(x)),
wt = Math.ceil(xScale(x + w)) - xt,
yt = Math.round(yScale(y)),
ht = Math.round(yScale(y - h)) - yt,
x0 = xt - lineWidth / 2,
y0 = Math.round(yt - ht / 2) - lineWidth / 2;
context.strokeRect(x0, y0, wt, ht);
context.fillRect(x0, y0, wt, ht);
});
},
extendRange : function (series) {
var
data = series.data,
xa = series.xaxis,
ya = series.yaxis,
w = series.timeline.barWidth;
if (xa.options.min === null)
xa.min = xa.datamin - w / 2;
if (xa.options.max === null) {
var
max = xa.max;
Flotr._.each(data, function (timeline) {
max = Math.max(max, timeline[0] + timeline[2]);
}, this);
xa.max = max + w / 2;
}
if (ya.options.min === null)
ya.min = ya.datamin - w;
if (ya.options.min === null)
ya.max = ya.datamax + w;
}
});
(function () {
var D = Flotr.DOM;
Flotr.addPlugin('crosshair', {
options: {
mode: null, // => one of null, 'x', 'y' or 'xy'
color: '#FF0000', // => crosshair color
hideCursor: true // => hide the cursor when the crosshair is shown
},
callbacks: {
'flotr:mousemove': function(e, pos) {
if (this.options.crosshair.mode) {
this.crosshair.clearCrosshair();
this.crosshair.drawCrosshair(pos);
}
}
},
/**
* Draws the selection box.
*/
drawCrosshair: function(pos) {
var octx = this.octx,
options = this.options.crosshair,
plotOffset = this.plotOffset,
x = plotOffset.left + Math.round(pos.relX) + .5,
y = plotOffset.top + Math.round(pos.relY) + .5;
if (pos.relX < 0 || pos.relY < 0 || pos.relX > this.plotWidth || pos.relY > this.plotHeight) {
this.el.style.cursor = null;
D.removeClass(this.el, 'flotr-crosshair');
return;
}
if (options.hideCursor) {
this.el.style.cursor = 'none';
D.addClass(this.el, 'flotr-crosshair');
}
octx.save();
octx.strokeStyle = options.color;
octx.lineWidth = 1;
octx.beginPath();
if (options.mode.indexOf('x') != -1) {
octx.moveTo(x, plotOffset.top);
octx.lineTo(x, plotOffset.top + this.plotHeight);
}
if (options.mode.indexOf('y') != -1) {
octx.moveTo(plotOffset.left, y);
octx.lineTo(plotOffset.left + this.plotWidth, y);
}
octx.stroke();
octx.restore();
},
/**
* Removes the selection box from the overlay canvas.
*/
clearCrosshair: function() {
var
plotOffset = this.plotOffset,
position = this.lastMousePos,
context = this.octx;
if (position) {
context.clearRect(
Math.round(position.relX) + plotOffset.left,
plotOffset.top,
1,
this.plotHeight + 1
);
context.clearRect(
plotOffset.left,
Math.round(position.relY) + plotOffset.top,
this.plotWidth + 1,
1
);
}
}
});
})();
(function() {
var
D = Flotr.DOM,
_ = Flotr._;
function getImage (type, canvas, width, height) {
// TODO add scaling for w / h
var
mime = 'image/'+type,
data = canvas.toDataURL(mime),
image = new Image();
image.src = data;
return image;
}
Flotr.addPlugin('download', {
saveImage: function (type, width, height, replaceCanvas) {
var image = null;
if (Flotr.isIE && Flotr.isIE < 9) {
image = '<html><body>'+this.canvas.firstChild.innerHTML+'</body></html>';
return window.open().document.write(image);
}
if (type !== 'jpeg' && type !== 'png') return;
image = getImage(type, this.canvas, width, height);
if (_.isElement(image) && replaceCanvas) {
this.download.restoreCanvas();
D.hide(this.canvas);
D.hide(this.overlay);
D.setStyles({position: 'absolute'});
D.insert(this.el, image);
this.saveImageElement = image;
} else {
return window.open(image.src);
}
},
restoreCanvas: function() {
D.show(this.canvas);
D.show(this.overlay);
if (this.saveImageElement) this.el.removeChild(this.saveImageElement);
this.saveImageElement = null;
}
});
})();
(function () {
var E = Flotr.EventAdapter,
_ = Flotr._;
Flotr.addPlugin('graphGrid', {
callbacks: {
'flotr:beforedraw' : function () {
this.graphGrid.drawGrid();
},
'flotr:afterdraw' : function () {
this.graphGrid.drawOutline();
}
},
drawGrid: function(){
var
ctx = this.ctx,
options = this.options,
grid = options.grid,
verticalLines = grid.verticalLines,
horizontalLines = grid.horizontalLines,
minorVerticalLines = grid.minorVerticalLines,
minorHorizontalLines = grid.minorHorizontalLines,
plotHeight = this.plotHeight,
plotWidth = this.plotWidth,
a, v, i, j;
if(verticalLines || minorVerticalLines ||
horizontalLines || minorHorizontalLines){
E.fire(this.el, 'flotr:beforegrid', [this.axes.x, this.axes.y, options, this]);
}
ctx.save();
ctx.lineWidth = 1;
ctx.strokeStyle = grid.tickColor;
function circularHorizontalTicks (ticks) {
for(i = 0; i < ticks.length; ++i){
var ratio = ticks[i].v / a.max;
for(j = 0; j <= sides; ++j){
ctx[j === 0 ? 'moveTo' : 'lineTo'](
Math.cos(j*coeff+angle)*radius*ratio,
Math.sin(j*coeff+angle)*radius*ratio
);
}
}
}
function drawGridLines (ticks, callback) {
_.each(_.pluck(ticks, 'v'), function(v){
// Don't show lines on upper and lower bounds.
if ((v <= a.min || v >= a.max) ||
(v == a.min || v == a.max) && grid.outlineWidth)
return;
callback(Math.floor(a.d2p(v)) + ctx.lineWidth/2);
});
}
function drawVerticalLines (x) {
ctx.moveTo(x, 0);
ctx.lineTo(x, plotHeight);
}
function drawHorizontalLines (y) {
ctx.moveTo(0, y);
ctx.lineTo(plotWidth, y);
}
if (grid.circular) {
ctx.translate(this.plotOffset.left+plotWidth/2, this.plotOffset.top+plotHeight/2);
var radius = Math.min(plotHeight, plotWidth)*options.radar.radiusRatio/2,
sides = this.axes.x.ticks.length,
coeff = 2*(Math.PI/sides),
angle = -Math.PI/2;
// Draw grid lines in vertical direction.
ctx.beginPath();
a = this.axes.y;
if(horizontalLines){
circularHorizontalTicks(a.ticks);
}
if(minorHorizontalLines){
circularHorizontalTicks(a.minorTicks);
}
if(verticalLines){
_.times(sides, function(i){
ctx.moveTo(0, 0);
ctx.lineTo(Math.cos(i*coeff+angle)*radius, Math.sin(i*coeff+angle)*radius);
});
}
ctx.stroke();
}
else {
ctx.translate(this.plotOffset.left, this.plotOffset.top);
// Draw grid background, if present in options.
if(grid.backgroundColor){
ctx.fillStyle = this.processColor(grid.backgroundColor, {x1: 0, y1: 0, x2: plotWidth, y2: plotHeight});
ctx.fillRect(0, 0, plotWidth, plotHeight);
}
ctx.beginPath();
a = this.axes.x;
if (verticalLines) drawGridLines(a.ticks, drawVerticalLines);
if (minorVerticalLines) drawGridLines(a.minorTicks, drawVerticalLines);
a = this.axes.y;
if (horizontalLines) drawGridLines(a.ticks, drawHorizontalLines);
if (minorHorizontalLines) drawGridLines(a.minorTicks, drawHorizontalLines);
ctx.stroke();
}
ctx.restore();
if(verticalLines || minorVerticalLines ||
horizontalLines || minorHorizontalLines){
E.fire(this.el, 'flotr:aftergrid', [this.axes.x, this.axes.y, options, this]);
}
},
drawOutline: function(){
var
that = this,
options = that.options,
grid = options.grid,
outline = grid.outline,
ctx = that.ctx,
backgroundImage = grid.backgroundImage,
plotOffset = that.plotOffset,
leftOffset = plotOffset.left,
topOffset = plotOffset.top,
plotWidth = that.plotWidth,
plotHeight = that.plotHeight,
v, img, src, left, top, globalAlpha;
if (!grid.outlineWidth) return;
ctx.save();
if (grid.circular) {
ctx.translate(leftOffset + plotWidth / 2, topOffset + plotHeight / 2);
var radius = Math.min(plotHeight, plotWidth) * options.radar.radiusRatio / 2,
sides = this.axes.x.ticks.length,
coeff = 2*(Math.PI/sides),
angle = -Math.PI/2;
// Draw axis/grid border.
ctx.beginPath();
ctx.lineWidth = grid.outlineWidth;
ctx.strokeStyle = grid.color;
ctx.lineJoin = 'round';
for(i = 0; i <= sides; ++i){
ctx[i === 0 ? 'moveTo' : 'lineTo'](Math.cos(i*coeff+angle)*radius, Math.sin(i*coeff+angle)*radius);
}
//ctx.arc(0, 0, radius, 0, Math.PI*2, true);
ctx.stroke();
}
else {
ctx.translate(leftOffset, topOffset);
// Draw axis/grid border.
var lw = grid.outlineWidth,
orig = 0.5-lw+((lw+1)%2/2),
lineTo = 'lineTo',
moveTo = 'moveTo';
ctx.lineWidth = lw;
ctx.strokeStyle = grid.color;
ctx.lineJoin = 'miter';
ctx.beginPath();
ctx.moveTo(orig, orig);
plotWidth = plotWidth - (lw / 2) % 1;
plotHeight = plotHeight + lw / 2;
ctx[outline.indexOf('n') !== -1 ? lineTo : moveTo](plotWidth, orig);
ctx[outline.indexOf('e') !== -1 ? lineTo : moveTo](plotWidth, plotHeight);
ctx[outline.indexOf('s') !== -1 ? lineTo : moveTo](orig, plotHeight);
ctx[outline.indexOf('w') !== -1 ? lineTo : moveTo](orig, orig);
ctx.stroke();
ctx.closePath();
}
ctx.restore();
if (backgroundImage) {
src = backgroundImage.src || backgroundImage;
left = (parseInt(backgroundImage.left, 10) || 0) + plotOffset.left;
top = (parseInt(backgroundImage.top, 10) || 0) + plotOffset.top;
img = new Image();
img.onload = function() {
ctx.save();
if (backgroundImage.alpha) ctx.globalAlpha = backgroundImage.alpha;
ctx.globalCompositeOperation = 'destination-over';
ctx.drawImage(img, 0, 0, img.width, img.height, left, top, plotWidth, plotHeight);
ctx.restore();
};
img.src = src;
}
}
});
})();
(function () {
var
D = Flotr.DOM,
_ = Flotr._,
flotr = Flotr,
S_MOUSETRACK = '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;';
Flotr.addPlugin('hit', {
callbacks: {
'flotr:mousemove': function(e, pos) {
this.hit.track(pos);
},
'flotr:click': function(pos) {
this.hit.track(pos);
},
'flotr:mouseout': function() {
this.hit.clearHit();
},
'flotr:destroy': function() {
this.mouseTrack = null;
}
},
track : function (pos) {
if (this.options.mouse.track || _.any(this.series, function(s){return s.mouse && s.mouse.track;})) {
this.hit.hit(pos);
}
},
/**
* Try a method on a graph type. If the method exists, execute it.
* @param {Object} series
* @param {String} method Method name.
* @param {Array} args Arguments applied to method.
* @return executed successfully or failed.
*/
executeOnType: function(s, method, args){
var
success = false,
options;
if (!_.isArray(s)) s = [s];
function e(s, index) {
_.each(_.keys(flotr.graphTypes), function (type) {
if (s[type] && s[type].show && this[type][method]) {
options = this.getOptions(s, type);
options.fill = !!s.mouse.fillColor;
options.fillStyle = this.processColor(s.mouse.fillColor || '#ffffff', {opacity: s.mouse.fillOpacity});
options.color = s.mouse.lineColor;
options.context = this.octx;
options.index = index;
if (args) options.args = args;
this[type][method].call(this[type], options);
success = true;
}
}, this);
}
_.each(s, e, this);
return success;
},
/**
* Updates the mouse tracking point on the overlay.
*/
drawHit: function(n){
var octx = this.octx,
s = n.series;
if (s.mouse.lineColor) {
octx.save();
octx.lineWidth = (s.points ? s.points.lineWidth : 1);
octx.strokeStyle = s.mouse.lineColor;
octx.fillStyle = this.processColor(s.mouse.fillColor || '#ffffff', {opacity: s.mouse.fillOpacity});
octx.translate(this.plotOffset.left, this.plotOffset.top);
if (!this.hit.executeOnType(s, 'drawHit', n)) {
var
xa = n.xaxis,
ya = n.yaxis;
octx.beginPath();
// TODO fix this (points) should move to general testable graph mixin
octx.arc(xa.d2p(n.x), ya.d2p(n.y), s.points.hitRadius || s.points.radius || s.mouse.radius, 0, 2 * Math.PI, true);
octx.fill();
octx.stroke();
octx.closePath();
}
octx.restore();
this.clip(octx);
}
this.prevHit = n;
},
/**
* Removes the mouse tracking point from the overlay.
*/
clearHit: function(){
var prev = this.prevHit,
octx = this.octx,
plotOffset = this.plotOffset;
octx.save();
octx.translate(plotOffset.left, plotOffset.top);
if (prev) {
if (!this.hit.executeOnType(prev.series, 'clearHit', this.prevHit)) {
// TODO fix this (points) should move to general testable graph mixin
var
s = prev.series,
lw = (s.points ? s.points.lineWidth : 1);
offset = (s.points.hitRadius || s.points.radius || s.mouse.radius) + lw;
octx.clearRect(
prev.xaxis.d2p(prev.x) - offset,
prev.yaxis.d2p(prev.y) - offset,
offset*2,
offset*2
);
}
D.hide(this.mouseTrack);
this.prevHit = null;
}
octx.restore();
},
/**
* Retrieves the nearest data point from the mouse cursor. If it's within
* a certain range, draw a point on the overlay canvas and display the x and y
* value of the data.
* @param {Object} mouse - Object that holds the relative x and y coordinates of the cursor.
*/
hit : function (mouse) {
var
options = this.options,
prevHit = this.prevHit,
closest, sensibility, dataIndex, seriesIndex, series, value, xaxis, yaxis, n;
if (this.series.length === 0) return;
// Nearest data element.
// dist, x, y, relX, relY, absX, absY, sAngle, eAngle, fraction, mouse,
// xaxis, yaxis, series, index, seriesIndex
n = {
relX : mouse.relX,
relY : mouse.relY,
absX : mouse.absX,
absY : mouse.absY
};
if (options.mouse.trackY &&
!options.mouse.trackAll &&
this.hit.executeOnType(this.series, 'hit', [mouse, n]) &&
!_.isUndefined(n.seriesIndex))
{
series = this.series[n.seriesIndex];
n.series = series;
n.mouse = series.mouse;
n.xaxis = series.xaxis;
n.yaxis = series.yaxis;
} else {
closest = this.hit.closest(mouse);
if (closest) {
closest = options.mouse.trackY ? closest.point : closest.x;
seriesIndex = closest.seriesIndex;
series = this.series[seriesIndex];
xaxis = series.xaxis;
yaxis = series.yaxis;
sensibility = 2 * series.mouse.sensibility;
if
(options.mouse.trackAll ||
(closest.distanceX < sensibility / xaxis.scale &&
(!options.mouse.trackY || closest.distanceY < sensibility / yaxis.scale)))
{
n.series = series;
n.xaxis = series.xaxis;
n.yaxis = series.yaxis;
n.mouse = series.mouse;
n.x = closest.x;
n.y = closest.y;
n.dist = closest.distance;
n.index = closest.dataIndex;
n.seriesIndex = seriesIndex;
}
}
}
if (!prevHit || (prevHit.index !== n.index || prevHit.seriesIndex !== n.seriesIndex)) {
this.hit.clearHit();
if (n.series && n.mouse && n.mouse.track) {
this.hit.drawMouseTrack(n);
this.hit.drawHit(n);
Flotr.EventAdapter.fire(this.el, 'flotr:hit', [n, this]);
}
}
},
closest : function (mouse) {
var
series = this.series,
options = this.options,
relX = mouse.relX,
relY = mouse.relY,
compare = Number.MAX_VALUE,
compareX = Number.MAX_VALUE,
closest = {},
closestX = {},
check = false,
serie, data,
distance, distanceX, distanceY,
mouseX, mouseY,
x, y, i, j;
function setClosest (o) {
o.distance = distance;
o.distanceX = distanceX;
o.distanceY = distanceY;
o.seriesIndex = i;
o.dataIndex = j;
o.x = x;
o.y = y;
check = true;
}
for (i = 0; i < series.length; i++) {
serie = series[i];
data = serie.data;
mouseX = serie.xaxis.p2d(relX);
mouseY = serie.yaxis.p2d(relY);
for (j = data.length; j--;) {
x = data[j][0];
y = data[j][1];
if (x === null || y === null) continue;
// don't check if the point isn't visible in the current range
if (x < serie.xaxis.min || x > serie.xaxis.max) continue;
distanceX = Math.abs(x - mouseX);
distanceY = Math.abs(y - mouseY);
// Skip square root for speed
distance = distanceX * distanceX + distanceY * distanceY;
if (distance < compare) {
compare = distance;
setClosest(closest);
}
if (distanceX < compareX) {
compareX = distanceX;
setClosest(closestX);
}
}
}
return check ? {
point : closest,
x : closestX
} : false;
},
drawMouseTrack : function (n) {
var
pos = '',
s = n.series,
p = n.mouse.position,
m = n.mouse.margin,
x = n.x,
y = n.y,
elStyle = S_MOUSETRACK,
mouseTrack = this.mouseTrack,
plotOffset = this.plotOffset,
left = plotOffset.left,
right = plotOffset.right,
bottom = plotOffset.bottom,
top = plotOffset.top,
decimals = n.mouse.trackDecimals,
options = this.options;
// Create
if (!mouseTrack) {
mouseTrack = D.node('<div class="flotr-mouse-value"></div>');
this.mouseTrack = mouseTrack;
D.insert(this.el, mouseTrack);
}
if (!n.mouse.relative) { // absolute to the canvas
if (p.charAt(0) == 'n') pos += 'top:' + (m + top) + 'px;bottom:auto;';
else if (p.charAt(0) == 's') pos += 'bottom:' + (m + bottom) + 'px;top:auto;';
if (p.charAt(1) == 'e') pos += 'right:' + (m + right) + 'px;left:auto;';
else if (p.charAt(1) == 'w') pos += 'left:' + (m + left) + 'px;right:auto;';
// Bars
} else if (s.bars && s.bars.show) {
pos += 'bottom:' + (m - top - n.yaxis.d2p(n.y/2) + this.canvasHeight) + 'px;top:auto;';
pos += 'left:' + (m + left + n.xaxis.d2p(n.x - options.bars.barWidth/2)) + 'px;right:auto;';
// Pie
} else if (s.pie && s.pie.show) {
var center = {
x: (this.plotWidth)/2,
y: (this.plotHeight)/2
},
radius = (Math.min(this.canvasWidth, this.canvasHeight) * s.pie.sizeRatio) / 2,
bisection = n.sAngle<n.eAngle ? (n.sAngle + n.eAngle) / 2: (n.sAngle + n.eAngle + 2* Math.PI) / 2;
pos += 'bottom:' + (m - top - center.y - Math.sin(bisection) * radius/2 + this.canvasHeight) + 'px;top:auto;';
pos += 'left:' + (m + left + center.x + Math.cos(bisection) * radius/2) + 'px;right:auto;';
// Default
} else {
if (/n/.test(p)) pos += 'bottom:' + (m - top - n.yaxis.d2p(n.y) + this.canvasHeight) + 'px;top:auto;';
else pos += 'top:' + (m + top + n.yaxis.d2p(n.y)) + 'px;bottom:auto;';
if (/e/.test(p)) pos += 'right:' + (m - left - n.xaxis.d2p(n.x) + this.canvasWidth) + 'px;left:auto;';
else pos += 'left:' + (m + left + n.xaxis.d2p(n.x)) + 'px;right:auto;';
}
elStyle += pos;
mouseTrack.style.cssText = elStyle;
if (!decimals || decimals < 0) decimals = 0;
if (x && x.toFixed) x = x.toFixed(decimals);
if (y && y.toFixed) y = y.toFixed(decimals);
mouseTrack.innerHTML = n.mouse.trackFormatter({
x: x ,
y: y,
series: n.series,
index: n.index,
nearest: n,
fraction: n.fraction
});
D.show(mouseTrack);
if (n.mouse.relative) {
if (!/[ew]/.test(p)) {
// Center Horizontally
mouseTrack.style.left =
(left + n.xaxis.d2p(n.x) - D.size(mouseTrack).width / 2) + 'px';
} else
if (!/[ns]/.test(p)) {
// Center Vertically
mouseTrack.style.top =
(top + n.yaxis.d2p(n.y) - D.size(mouseTrack).height / 2) + 'px';
}
}
}
});
})();
/**
* Selection Handles Plugin
*
*
* Options
* show - True enables the handles plugin.
* drag - Left and Right drag handles
* scroll - Scrolling handle
*/
(function () {
function isLeftClick (e, type) {
return (e.which ? (e.which === 1) : (e.button === 0 || e.button === 1));
}
function boundX(x, graph) {
return Math.min(Math.max(0, x), graph.plotWidth - 1);
}
function boundY(y, graph) {
return Math.min(Math.max(0, y), graph.plotHeight);
}
var
D = Flotr.DOM,
E = Flotr.EventAdapter,
_ = Flotr._;
Flotr.addPlugin('selection', {
options: {
pinchOnly: null, // Only select on pinch
mode: null, // => one of null, 'x', 'y' or 'xy'
color: '#B6D9FF', // => selection box color
fps: 20 // => frames-per-second
},
callbacks: {
'flotr:mouseup' : function (event) {
var
options = this.options.selection,
selection = this.selection,
pointer = this.getEventPosition(event);
if (!options || !options.mode) return;
if (selection.interval) clearInterval(selection.interval);
if (this.multitouches) {
selection.updateSelection();
} else
if (!options.pinchOnly) {
selection.setSelectionPos(selection.selection.second, pointer);
}
selection.clearSelection();
if(selection.selecting && selection.selectionIsSane()){
selection.drawSelection();
selection.fireSelectEvent();
this.ignoreClick = true;
}
},
'flotr:mousedown' : function (event) {
var
options = this.options.selection,
selection = this.selection,
pointer = this.getEventPosition(event);
if (!options || !options.mode) return;
if (!options.mode || (!isLeftClick(event) && _.isUndefined(event.touches))) return;
if (!options.pinchOnly) selection.setSelectionPos(selection.selection.first, pointer);
if (selection.interval) clearInterval(selection.interval);
this.lastMousePos.pageX = null;
selection.selecting = false;
selection.interval = setInterval(
_.bind(selection.updateSelection, this),
1000 / options.fps
);
},
'flotr:destroy' : function (event) {
clearInterval(this.selection.interval);
}
},
// TODO This isn't used. Maybe it belongs in the draw area and fire select event methods?
getArea: function() {
var s = this.selection.selection,
first = s.first,
second = s.second;
return {
x1: Math.min(first.x, second.x),
x2: Math.max(first.x, second.x),
y1: Math.min(first.y, second.y),
y2: Math.max(first.y, second.y)
};
},
selection: {first: {x: -1, y: -1}, second: {x: -1, y: -1}},
prevSelection: null,
interval: null,
/**
* Fires the 'flotr:select' event when the user made a selection.
*/
fireSelectEvent: function(name){
var a = this.axes,
s = this.selection.selection,
x1, x2, y1, y2;
name = name || 'select';
x1 = a.x.p2d(s.first.x);
x2 = a.x.p2d(s.second.x);
y1 = a.y.p2d(s.first.y);
y2 = a.y.p2d(s.second.y);
E.fire(this.el, 'flotr:'+name, [{
x1:Math.min(x1, x2),
y1:Math.min(y1, y2),
x2:Math.max(x1, x2),
y2:Math.max(y1, y2),
xfirst:x1, xsecond:x2, yfirst:y1, ysecond:y2,
selection : s
}, this]);
},
/**
* Allows the user the manually select an area.
* @param {Object} area - Object with coordinates to select.
*/
setSelection: function(area, preventEvent){
var options = this.options,
xa = this.axes.x,
ya = this.axes.y,
vertScale = ya.scale,
hozScale = xa.scale,
selX = options.selection.mode.indexOf('x') != -1,
selY = options.selection.mode.indexOf('y') != -1,
s = this.selection.selection;
this.selection.clearSelection();
s.first.y = boundY((selX && !selY) ? 0 : (ya.max - area.y1) * vertScale, this);
s.second.y = boundY((selX && !selY) ? this.plotHeight - 1: (ya.max - area.y2) * vertScale, this);
s.first.x = boundX((selY && !selX) ? 0 : (area.x1 - xa.min) * hozScale, this);
s.second.x = boundX((selY && !selX) ? this.plotWidth : (area.x2 - xa.min) * hozScale, this);
this.selection.drawSelection();
if (!preventEvent)
this.selection.fireSelectEvent();
},
/**
* Calculates the position of the selection.
* @param {Object} pos - Position object.
* @param {Event} event - Event object.
*/
setSelectionPos: function(pos, pointer) {
var mode = this.options.selection.mode,
selection = this.selection.selection;
if(mode.indexOf('x') == -1) {
pos.x = (pos == selection.first) ? 0 : this.plotWidth;
}else{
pos.x = boundX(pointer.relX, this);
}
if (mode.indexOf('y') == -1) {
pos.y = (pos == selection.first) ? 0 : this.plotHeight - 1;
}else{
pos.y = boundY(pointer.relY, this);
}
},
/**
* Draws the selection box.
*/
drawSelection: function() {
this.selection.fireSelectEvent('selecting');
var s = this.selection.selection,
octx = this.octx,
options = this.options,
plotOffset = this.plotOffset,
prevSelection = this.selection.prevSelection;
if (prevSelection &&
s.first.x == prevSelection.first.x &&
s.first.y == prevSelection.first.y &&
s.second.x == prevSelection.second.x &&
s.second.y == prevSelection.second.y) {
return;
}
octx.save();
octx.strokeStyle = this.processColor(options.selection.color, {opacity: 0.8});
octx.lineWidth = 1;
octx.lineJoin = 'miter';
octx.fillStyle = this.processColor(options.selection.color, {opacity: 0.4});
this.selection.prevSelection = {
first: { x: s.first.x, y: s.first.y },
second: { x: s.second.x, y: s.second.y }
};
var x = Math.min(s.first.x, s.second.x),
y = Math.min(s.first.y, s.second.y),
w = Math.abs(s.second.x - s.first.x),
h = Math.abs(s.second.y - s.first.y);
octx.fillRect(x + plotOffset.left+0.5, y + plotOffset.top+0.5, w, h);
octx.strokeRect(x + plotOffset.left+0.5, y + plotOffset.top+0.5, w, h);
octx.restore();
},
/**
* Updates (draws) the selection box.
*/
updateSelection: function(){
if (!this.lastMousePos.pageX) return;
this.selection.selecting = true;
if (this.multitouches) {
this.selection.setSelectionPos(this.selection.selection.first, this.getEventPosition(this.multitouches[0]));
this.selection.setSelectionPos(this.selection.selection.second, this.getEventPosition(this.multitouches[1]));
} else
if (this.options.selection.pinchOnly) {
return;
} else {
this.selection.setSelectionPos(this.selection.selection.second, this.lastMousePos);
}
this.selection.clearSelection();
if(this.selection.selectionIsSane()) {
this.selection.drawSelection();
}
},
/**
* Removes the selection box from the overlay canvas.
*/
clearSelection: function() {
if (!this.selection.prevSelection) return;
var prevSelection = this.selection.prevSelection,
lw = 1,
plotOffset = this.plotOffset,
x = Math.min(prevSelection.first.x, prevSelection.second.x),
y = Math.min(prevSelection.first.y, prevSelection.second.y),
w = Math.abs(prevSelection.second.x - prevSelection.first.x),
h = Math.abs(prevSelection.second.y - prevSelection.first.y);
this.octx.clearRect(x + plotOffset.left - lw + 0.5,
y + plotOffset.top - lw,
w + 2 * lw + 0.5,
h + 2 * lw + 0.5);
this.selection.prevSelection = null;
},
/**
* Determines whether or not the selection is sane and should be drawn.
* @return {Boolean} - True when sane, false otherwise.
*/
selectionIsSane: function(){
var s = this.selection.selection;
return Math.abs(s.second.x - s.first.x) >= 5 ||
Math.abs(s.second.y - s.first.y) >= 5;
}
});
})();
(function () {
var D = Flotr.DOM;
Flotr.addPlugin('labels', {
callbacks : {
'flotr:afterdraw' : function () {
this.labels.draw();
}
},
draw: function(){
// Construct fixed width label boxes, which can be styled easily.
var
axis, tick, left, top, xBoxWidth,
radius, sides, coeff, angle,
div, i, html = '',
noLabels = 0,
options = this.options,
ctx = this.ctx,
a = this.axes,
style = { size: options.fontSize };
for (i = 0; i < a.x.ticks.length; ++i){
if (a.x.ticks[i].label) { ++noLabels; }
}
xBoxWidth = this.plotWidth / noLabels;
if (options.grid.circular) {
ctx.save();
ctx.translate(this.plotOffset.left + this.plotWidth / 2,
this.plotOffset.top + this.plotHeight / 2);
radius = this.plotHeight * options.radar.radiusRatio / 2 + options.fontSize;
sides = this.axes.x.ticks.length;
coeff = 2 * (Math.PI / sides);
angle = -Math.PI / 2;
drawLabelCircular(this, a.x, false);
drawLabelCircular(this, a.x, true);
drawLabelCircular(this, a.y, false);
drawLabelCircular(this, a.y, true);
ctx.restore();
}
if (!options.HtmlText && this.textEnabled) {
drawLabelNoHtmlText(this, a.x, 'center', 'top');
drawLabelNoHtmlText(this, a.x2, 'center', 'bottom');
drawLabelNoHtmlText(this, a.y, 'right', 'middle');
drawLabelNoHtmlText(this, a.y2, 'left', 'middle');
} else if ((
a.x.options.showLabels ||
a.x2.options.showLabels ||
a.y.options.showLabels ||
a.y2.options.showLabels) &&
!options.grid.circular
) {
html = '';
drawLabelHtml(this, a.x);
drawLabelHtml(this, a.x2);
drawLabelHtml(this, a.y);
drawLabelHtml(this, a.y2);
ctx.stroke();
ctx.restore();
div = D.create('div');
D.setStyles(div, {
fontSize: 'smaller',
color: options.grid.color
});
div.className = 'flotr-labels';
D.insert(this.el, div);
D.insert(div, html);
}
function drawLabelCircular (graph, axis, minorTicks) {
var
ticks = minorTicks ? axis.minorTicks : axis.ticks,
isX = axis.orientation === 1,
isFirst = axis.n === 1,
style, offset;
style = {
color : axis.options.color || options.grid.color,
angle : Flotr.toRad(axis.options.labelsAngle),
textBaseline : 'middle'
};
for (i = 0; i < ticks.length &&
(minorTicks ? axis.options.showMinorLabels : axis.options.showLabels); ++i){
tick = ticks[i];
tick.label += '';
if (!tick.label || !tick.label.length) { continue; }
x = Math.cos(i * coeff + angle) * radius;
y = Math.sin(i * coeff + angle) * radius;
style.textAlign = isX ? (Math.abs(x) < 0.1 ? 'center' : (x < 0 ? 'right' : 'left')) : 'left';
Flotr.drawText(
ctx, tick.label,
isX ? x : 3,
isX ? y : -(axis.ticks[i].v / axis.max) * (radius - options.fontSize),
style
);
}
}
function drawLabelNoHtmlText (graph, axis, textAlign, textBaseline) {
var
isX = axis.orientation === 1,
isFirst = axis.n === 1,
style, offset;
style = {
color : axis.options.color || options.grid.color,
textAlign : textAlign,
textBaseline : textBaseline,
angle : Flotr.toRad(axis.options.labelsAngle)
};
style = Flotr.getBestTextAlign(style.angle, style);
for (i = 0; i < axis.ticks.length && continueShowingLabels(axis); ++i) {
tick = axis.ticks[i];
if (!tick.label || !tick.label.length) { continue; }
offset = axis.d2p(tick.v);
if (offset < 0 ||
offset > (isX ? graph.plotWidth : graph.plotHeight)) { continue; }
Flotr.drawText(
ctx, tick.label,
leftOffset(graph, isX, isFirst, offset),
topOffset(graph, isX, isFirst, offset),
style
);
// Only draw on axis y2
if (!isX && !isFirst) {
ctx.save();
ctx.strokeStyle = style.color;
ctx.beginPath();
ctx.moveTo(graph.plotOffset.left + graph.plotWidth - 8, graph.plotOffset.top + axis.d2p(tick.v));
ctx.lineTo(graph.plotOffset.left + graph.plotWidth, graph.plotOffset.top + axis.d2p(tick.v));
ctx.stroke();
ctx.restore();
}
}
function continueShowingLabels (axis) {
return axis.options.showLabels && axis.used;
}
function leftOffset (graph, isX, isFirst, offset) {
return graph.plotOffset.left +
(isX ? offset :
(isFirst ?
-options.grid.labelMargin :
options.grid.labelMargin + graph.plotWidth));
}
function topOffset (graph, isX, isFirst, offset) {
return graph.plotOffset.top +
(isX ? options.grid.labelMargin : offset) +
((isX && isFirst) ? graph.plotHeight : 0);
}
}
function drawLabelHtml (graph, axis) {
var
isX = axis.orientation === 1,
isFirst = axis.n === 1,
name = '',
left, style, top,
offset = graph.plotOffset;
if (!isX && !isFirst) {
ctx.save();
ctx.strokeStyle = axis.options.color || options.grid.color;
ctx.beginPath();
}
if (axis.options.showLabels && (isFirst ? true : axis.used)) {
for (i = 0; i < axis.ticks.length; ++i) {
tick = axis.ticks[i];
if (!tick.label || !tick.label.length ||
((isX ? offset.left : offset.top) + axis.d2p(tick.v) < 0) ||
((isX ? offset.left : offset.top) + axis.d2p(tick.v) > (isX ? graph.canvasWidth : graph.canvasHeight))) {
continue;
}
top = offset.top +
(isX ?
((isFirst ? 1 : -1 ) * (graph.plotHeight + options.grid.labelMargin)) :
axis.d2p(tick.v) - axis.maxLabel.height / 2);
left = isX ? (offset.left + axis.d2p(tick.v) - xBoxWidth / 2) : 0;
name = '';
if (i === 0) {
name = ' first';
} else if (i === axis.ticks.length - 1) {
name = ' last';
}
name += isX ? ' flotr-grid-label-x' : ' flotr-grid-label-y';
html += [
'<div style="position:absolute; text-align:' + (isX ? 'center' : 'right') + '; ',
'top:' + top + 'px; ',
((!isX && !isFirst) ? 'right:' : 'left:') + left + 'px; ',
'width:' + (isX ? xBoxWidth : ((isFirst ? offset.left : offset.right) - options.grid.labelMargin)) + 'px; ',
axis.options.color ? ('color:' + axis.options.color + '; ') : ' ',
'" class="flotr-grid-label' + name + '">' + tick.label + '</div>'
].join(' ');
if (!isX && !isFirst) {
ctx.moveTo(offset.left + graph.plotWidth - 8, offset.top + axis.d2p(tick.v));
ctx.lineTo(offset.left + graph.plotWidth, offset.top + axis.d2p(tick.v));
}
}
}
}
}
});
})();
(function () {
var
D = Flotr.DOM,
_ = Flotr._;
Flotr.addPlugin('legend', {
options: {
show: true, // => setting to true will show the legend, hide otherwise
noColumns: 1, // => number of colums in legend table // @todo: doesn't work for HtmlText = false
labelFormatter: function(v){return v;}, // => fn: string -> string
labelBoxBorderColor: '#CCCCCC', // => border color for the little label boxes
labelBoxWidth: 14,
labelBoxHeight: 10,
labelBoxMargin: 5,
labelBoxOpacity: 0.4,
container: null, // => container (as jQuery object) to put legend in, null means default on top of graph
position: 'nw', // => position of default legend container within plot
margin: 5, // => distance from grid edge to default legend container within plot
backgroundColor: '#F0F0F0', // => Legend background color.
backgroundOpacity: 0.85// => set to 0 to avoid background, set to 1 for a solid background
},
callbacks: {
'flotr:afterinit': function() {
this.legend.insertLegend();
}
},
/**
* Adds a legend div to the canvas container or draws it on the canvas.
*/
insertLegend: function(){
if(!this.options.legend.show)
return;
var series = this.series,
plotOffset = this.plotOffset,
options = this.options,
legend = options.legend,
fragments = [],
rowStarted = false,
ctx = this.ctx,
itemCount = _.filter(series, function(s) {return (s.label && !s.hide);}).length,
p = legend.position,
m = legend.margin,
i, label, color;
if (itemCount) {
if (!options.HtmlText && this.textEnabled && !legend.container) {
var style = {
size: options.fontSize*1.1,
color: options.grid.color
};
var lbw = legend.labelBoxWidth,
lbh = legend.labelBoxHeight,
lbm = legend.labelBoxMargin,
offsetX = plotOffset.left + m,
offsetY = plotOffset.top + m;
// We calculate the labels' max width
var labelMaxWidth = 0;
for(i = series.length - 1; i > -1; --i){
if(!series[i].label || series[i].hide) continue;
label = legend.labelFormatter(series[i].label);
labelMaxWidth = Math.max(labelMaxWidth, this._text.measureText(label, style).width);
}
var legendWidth = Math.round(lbw + lbm*3 + labelMaxWidth),
legendHeight = Math.round(itemCount*(lbm+lbh) + lbm);
if(p.charAt(0) == 's') offsetY = plotOffset.top + this.plotHeight - (m + legendHeight);
if(p.charAt(1) == 'e') offsetX = plotOffset.left + this.plotWidth - (m + legendWidth);
// Legend box
color = this.processColor(legend.backgroundColor, {opacity: legend.backgroundOpacity || 0.1});
ctx.fillStyle = color;
ctx.fillRect(offsetX, offsetY, legendWidth, legendHeight);
ctx.strokeStyle = legend.labelBoxBorderColor;
ctx.strokeRect(Flotr.toPixel(offsetX), Flotr.toPixel(offsetY), legendWidth, legendHeight);
// Legend labels
var x = offsetX + lbm;
var y = offsetY + lbm;
for(i = 0; i < series.length; i++){
if(!series[i].label || series[i].hide) continue;
label = legend.labelFormatter(series[i].label);
ctx.fillStyle = series[i].color;
ctx.fillRect(x, y, lbw-1, lbh-1);
ctx.strokeStyle = legend.labelBoxBorderColor;
ctx.lineWidth = 1;
ctx.strokeRect(Math.ceil(x)-1.5, Math.ceil(y)-1.5, lbw+2, lbh+2);
// Legend text
Flotr.drawText(ctx, label, x + lbw + lbm, y + lbh, style);
y += lbh + lbm;
}
}
else {
for(i = 0; i < series.length; ++i){
if(!series[i].label || series[i].hide) continue;
if(i % legend.noColumns === 0){
fragments.push(rowStarted ? '</tr><tr>' : '<tr>');
rowStarted = true;
}
// @TODO remove requirement on bars
var s = series[i],
boxWidth = legend.labelBoxWidth,
boxHeight = legend.labelBoxHeight,
opacityValue = (s.bars ? s.bars.fillOpacity : legend.labelBoxOpacity),
opacity = 'opacity:' + opacityValue + ';filter:alpha(opacity=' + opacityValue*100 + ');';
label = legend.labelFormatter(s.label);
color = 'background-color:' + ((s.bars && s.bars.show && s.bars.fillColor && s.bars.fill) ? s.bars.fillColor : s.color) + ';';
fragments.push(
'<td class="flotr-legend-color-box">',
'<div style="border:1px solid ', legend.labelBoxBorderColor, ';padding:1px">',
'<div style="width:', (boxWidth-1), 'px;height:', (boxHeight-1), 'px;border:1px solid ', series[i].color, '">', // Border
'<div style="width:', boxWidth, 'px;height:', boxHeight, 'px;', 'opacity:.4;', color, '"></div>', // Background
'</div>',
'</div>',
'</td>',
'<td class="flotr-legend-label">', label, '</td>'
);
}
if(rowStarted) fragments.push('</tr>');
if(fragments.length > 0){
var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join('') + '</table>';
if(legend.container){
D.empty(legend.container);
D.insert(legend.container, table);
}
else {
var styles = {position: 'absolute', 'zIndex': '2', 'border' : '1px solid ' + legend.labelBoxBorderColor};
if(p.charAt(0) == 'n') { styles.top = (m + plotOffset.top) + 'px'; styles.bottom = 'auto'; }
else if(p.charAt(0) == 's') { styles.bottom = (m + plotOffset.bottom) + 'px'; styles.top = 'auto'; }
if(p.charAt(1) == 'e') { styles.right = (m + plotOffset.right) + 'px'; styles.left = 'auto'; }
else if(p.charAt(1) == 'w') { styles.left = (m + plotOffset.left) + 'px'; styles.right = 'auto'; }
var div = D.create('div'), size;
div.className = 'flotr-legend';
D.setStyles(div, styles);
D.insert(div, table);
D.insert(this.el, div);
if(!legend.backgroundOpacity)
return;
var c = legend.backgroundColor || options.grid.backgroundColor || '#ffffff';
_.extend(styles, D.size(div), {
'backgroundColor': c,
'zIndex' : '',
'border' : ''
});
styles.width += 'px';
styles.height += 'px';
// Put in the transparent background separately to avoid blended labels and
div = D.create('div');
div.className = 'flotr-legend-bg';
D.setStyles(div, styles);
D.opacity(div, legend.backgroundOpacity);
D.insert(div, ' ');
D.insert(this.el, div);
}
}
}
}
}
});
})();
/** Spreadsheet **/
(function() {
function getRowLabel(value){
if (this.options.spreadsheet.tickFormatter){
//TODO maybe pass the xaxis formatter to the custom tick formatter as an opt-out?
return this.options.spreadsheet.tickFormatter(value);
}
else {
var t = _.find(this.axes.x.ticks, function(t){return t.v == value;});
if (t) {
return t.label;
}
return value;
}
}
var
D = Flotr.DOM,
_ = Flotr._;
Flotr.addPlugin('spreadsheet', {
options: {
show: false, // => show the data grid using two tabs
tabGraphLabel: 'Graph',
tabDataLabel: 'Data',
toolbarDownload: 'Download CSV', // @todo: add better language support
toolbarSelectAll: 'Select all',
csvFileSeparator: ',',
decimalSeparator: '.',
tickFormatter: null,
initialTab: 'graph'
},
/**
* Builds the tabs in the DOM
*/
callbacks: {
'flotr:afterconstruct': function(){
// @TODO necessary?
//this.el.select('.flotr-tabs-group,.flotr-datagrid-container').invoke('remove');
if (!this.options.spreadsheet.show) return;
var ss = this.spreadsheet,
container = D.node('<div class="flotr-tabs-group" style="position:absolute;left:0px;width:'+this.canvasWidth+'px"></div>'),
graph = D.node('<div style="float:left" class="flotr-tab selected">'+this.options.spreadsheet.tabGraphLabel+'</div>'),
data = D.node('<div style="float:left" class="flotr-tab">'+this.options.spreadsheet.tabDataLabel+'</div>'),
offset;
ss.tabsContainer = container;
ss.tabs = { graph : graph, data : data };
D.insert(container, graph);
D.insert(container, data);
D.insert(this.el, container);
offset = D.size(data).height + 2;
this.plotOffset.bottom += offset;
D.setStyles(container, {top: this.canvasHeight-offset+'px'});
this.
observe(graph, 'click', function(){ss.showTab('graph');}).
observe(data, 'click', function(){ss.showTab('data');});
if (this.options.spreadsheet.initialTab !== 'graph'){
ss.showTab(this.options.spreadsheet.initialTab);
}
}
},
/**
* Builds a matrix of the data to make the correspondance between the x values and the y values :
* X value => Y values from the axes
* @return {Array} The data grid
*/
loadDataGrid: function(){
if (this.seriesData) return this.seriesData;
var s = this.series,
rows = {};
/* The data grid is a 2 dimensions array. There is a row for each X value.
* Each row contains the x value and the corresponding y value for each serie ('undefined' if there isn't one)
**/
_.each(s, function(serie, i){
_.each(serie.data, function (v) {
var x = v[0],
y = v[1],
r = rows[x];
if (r) {
r[i+1] = y;
} else {
var newRow = [];
newRow[0] = x;
newRow[i+1] = y;
rows[x] = newRow;
}
});
});
// The data grid is sorted by x value
this.seriesData = _.sortBy(rows, function(row, x){
return parseInt(x, 10);
});
return this.seriesData;
},
/**
* Constructs the data table for the spreadsheet
* @todo make a spreadsheet manager (Flotr.Spreadsheet)
* @return {Element} The resulting table element
*/
constructDataGrid: function(){
// If the data grid has already been built, nothing to do here
if (this.spreadsheet.datagrid) return this.spreadsheet.datagrid;
var s = this.series,
datagrid = this.spreadsheet.loadDataGrid(),
colgroup = ['<colgroup><col />'],
buttonDownload, buttonSelect, t;
// First row : series' labels
var html = ['<table class="flotr-datagrid"><tr class="first-row">'];
html.push('<th>&nbsp;</th>');
_.each(s, function(serie,i){
html.push('<th scope="col">'+(serie.label || String.fromCharCode(65+i))+'</th>');
colgroup.push('<col />');
});
html.push('</tr>');
// Data rows
_.each(datagrid, function(row){
html.push('<tr>');
_.times(s.length+1, function(i){
var tag = 'td',
value = row[i],
// TODO: do we really want to handle problems with floating point
// precision here?
content = (!_.isUndefined(value) ? Math.round(value*100000)/100000 : '');
if (i === 0) {
tag = 'th';
var label = getRowLabel.call(this, content);
if (label) content = label;
}
html.push('<'+tag+(tag=='th'?' scope="row"':'')+'>'+content+'</'+tag+'>');
}, this);
html.push('</tr>');
}, this);
colgroup.push('</colgroup>');
t = D.node(html.join(''));
/**
* @TODO disabled this
if (!Flotr.isIE || Flotr.isIE == 9) {
function handleMouseout(){
t.select('colgroup col.hover, th.hover').invoke('removeClassName', 'hover');
}
function handleMouseover(e){
var td = e.element(),
siblings = td.previousSiblings();
t.select('th[scope=col]')[siblings.length-1].addClassName('hover');
t.select('colgroup col')[siblings.length].addClassName('hover');
}
_.each(t.select('td'), function(td) {
Flotr.EventAdapter.
observe(td, 'mouseover', handleMouseover).
observe(td, 'mouseout', handleMouseout);
});
}
*/
buttonDownload = D.node(
'<button type="button" class="flotr-datagrid-toolbar-button">' +
this.options.spreadsheet.toolbarDownload +
'</button>');
buttonSelect = D.node(
'<button type="button" class="flotr-datagrid-toolbar-button">' +
this.options.spreadsheet.toolbarSelectAll+
'</button>');
this.
observe(buttonDownload, 'click', _.bind(this.spreadsheet.downloadCSV, this)).
observe(buttonSelect, 'click', _.bind(this.spreadsheet.selectAllData, this));
var toolbar = D.node('<div class="flotr-datagrid-toolbar"></div>');
D.insert(toolbar, buttonDownload);
D.insert(toolbar, buttonSelect);
var containerHeight =this.canvasHeight - D.size(this.spreadsheet.tabsContainer).height-2,
container = D.node('<div class="flotr-datagrid-container" style="position:absolute;left:0px;top:0px;width:'+
this.canvasWidth+'px;height:'+containerHeight+'px;overflow:auto;z-index:10"></div>');
D.insert(container, toolbar);
D.insert(container, t);
D.insert(this.el, container);
this.spreadsheet.datagrid = t;
this.spreadsheet.container = container;
return t;
},
/**
* Shows the specified tab, by its name
* @todo make a tab manager (Flotr.Tabs)
* @param {String} tabName - The tab name
*/
showTab: function(tabName){
if (this.spreadsheet.activeTab === tabName){
return;
}
switch(tabName) {
case 'graph':
D.hide(this.spreadsheet.container);
D.removeClass(this.spreadsheet.tabs.data, 'selected');
D.addClass(this.spreadsheet.tabs.graph, 'selected');
break;
case 'data':
if (!this.spreadsheet.datagrid)
this.spreadsheet.constructDataGrid();
D.show(this.spreadsheet.container);
D.addClass(this.spreadsheet.tabs.data, 'selected');
D.removeClass(this.spreadsheet.tabs.graph, 'selected');
break;
default:
throw 'Illegal tab name: ' + tabName;
}
this.spreadsheet.activeTab = tabName;
},
/**
* Selects the data table in the DOM for copy/paste
*/
selectAllData: function(){
if (this.spreadsheet.tabs) {
var selection, range, doc, win, node = this.spreadsheet.constructDataGrid();
this.spreadsheet.showTab('data');
// deferred to be able to select the table
setTimeout(function () {
if ((doc = node.ownerDocument) && (win = doc.defaultView) &&
win.getSelection && doc.createRange &&
(selection = window.getSelection()) &&
selection.removeAllRanges) {
range = doc.createRange();
range.selectNode(node);
selection.removeAllRanges();
selection.addRange(range);
}
else if (document.body && document.body.createTextRange &&
(range = document.body.createTextRange())) {
range.moveToElementText(node);
range.select();
}
}, 0);
return true;
}
else return false;
},
/**
* Converts the data into CSV in order to download a file
*/
downloadCSV: function(){
var csv = '',
series = this.series,
options = this.options,
dg = this.spreadsheet.loadDataGrid(),
separator = encodeURIComponent(options.spreadsheet.csvFileSeparator);
if (options.spreadsheet.decimalSeparator === options.spreadsheet.csvFileSeparator) {
throw "The decimal separator is the same as the column separator ("+options.spreadsheet.decimalSeparator+")";
}
// The first row
_.each(series, function(serie, i){
csv += separator+'"'+(serie.label || String.fromCharCode(65+i)).replace(/\"/g, '\\"')+'"';
});
csv += "%0D%0A"; // \r\n
// For each row
csv += _.reduce(dg, function(memo, row){
var rowLabel = getRowLabel.call(this, row[0]) || '';
rowLabel = '"'+(rowLabel+'').replace(/\"/g, '\\"')+'"';
var numbers = row.slice(1).join(separator);
if (options.spreadsheet.decimalSeparator !== '.') {
numbers = numbers.replace(/\./g, options.spreadsheet.decimalSeparator);
}
return memo + rowLabel+separator+numbers+"%0D%0A"; // \t and \r\n
}, '', this);
if (Flotr.isIE && Flotr.isIE < 9) {
csv = csv.replace(new RegExp(separator, 'g'), decodeURIComponent(separator)).replace(/%0A/g, '\n').replace(/%0D/g, '\r');
window.open().document.write(csv);
}
else window.open('data:text/csv,'+csv);
}
});
})();
(function () {
var D = Flotr.DOM;
Flotr.addPlugin('titles', {
callbacks: {
'flotr:afterdraw': function() {
this.titles.drawTitles();
}
},
/**
* Draws the title and the subtitle
*/
drawTitles : function () {
var html,
options = this.options,
margin = options.grid.labelMargin,
ctx = this.ctx,
a = this.axes;
if (!options.HtmlText && this.textEnabled) {
var style = {
size: options.fontSize,
color: options.grid.color,
textAlign: 'center'
};
// Add subtitle
if (options.subtitle){
Flotr.drawText(
ctx, options.subtitle,
this.plotOffset.left + this.plotWidth/2,
this.titleHeight + this.subtitleHeight - 2,
style
);
}
style.weight = 1.5;
style.size *= 1.5;
// Add title
if (options.title){
Flotr.drawText(
ctx, options.title,
this.plotOffset.left + this.plotWidth/2,
this.titleHeight - 2,
style
);
}
style.weight = 1.8;
style.size *= 0.8;
// Add x axis title
if (a.x.options.title && a.x.used){
style.textAlign = a.x.options.titleAlign || 'center';
style.textBaseline = 'top';
style.angle = Flotr.toRad(a.x.options.titleAngle);
style = Flotr.getBestTextAlign(style.angle, style);
Flotr.drawText(
ctx, a.x.options.title,
this.plotOffset.left + this.plotWidth/2,
this.plotOffset.top + a.x.maxLabel.height + this.plotHeight + 2 * margin,
style
);
}
// Add x2 axis title
if (a.x2.options.title && a.x2.used){
style.textAlign = a.x2.options.titleAlign || 'center';
style.textBaseline = 'bottom';
style.angle = Flotr.toRad(a.x2.options.titleAngle);
style = Flotr.getBestTextAlign(style.angle, style);
Flotr.drawText(
ctx, a.x2.options.title,
this.plotOffset.left + this.plotWidth/2,
this.plotOffset.top - a.x2.maxLabel.height - 2 * margin,
style
);
}
// Add y axis title
if (a.y.options.title && a.y.used){
style.textAlign = a.y.options.titleAlign || 'right';
style.textBaseline = 'middle';
style.angle = Flotr.toRad(a.y.options.titleAngle);
style = Flotr.getBestTextAlign(style.angle, style);
Flotr.drawText(
ctx, a.y.options.title,
this.plotOffset.left - a.y.maxLabel.width - 2 * margin,
this.plotOffset.top + this.plotHeight / 2,
style
);
}
// Add y2 axis title
if (a.y2.options.title && a.y2.used){
style.textAlign = a.y2.options.titleAlign || 'left';
style.textBaseline = 'middle';
style.angle = Flotr.toRad(a.y2.options.titleAngle);
style = Flotr.getBestTextAlign(style.angle, style);
Flotr.drawText(
ctx, a.y2.options.title,
this.plotOffset.left + this.plotWidth + a.y2.maxLabel.width + 2 * margin,
this.plotOffset.top + this.plotHeight / 2,
style
);
}
}
else {
html = [];
// Add title
if (options.title)
html.push(
'<div style="position:absolute;top:0;left:',
this.plotOffset.left, 'px;font-size:1em;font-weight:bold;text-align:center;width:',
this.plotWidth,'px;" class="flotr-title">', options.title, '</div>'
);
// Add subtitle
if (options.subtitle)
html.push(
'<div style="position:absolute;top:', this.titleHeight, 'px;left:',
this.plotOffset.left, 'px;font-size:smaller;text-align:center;width:',
this.plotWidth, 'px;" class="flotr-subtitle">', options.subtitle, '</div>'
);
html.push('</div>');
html.push('<div class="flotr-axis-title" style="font-weight:bold;">');
// Add x axis title
if (a.x.options.title && a.x.used)
html.push(
'<div style="position:absolute;top:',
(this.plotOffset.top + this.plotHeight + options.grid.labelMargin + a.x.titleSize.height),
'px;left:', this.plotOffset.left, 'px;width:', this.plotWidth,
'px;text-align:', a.x.options.titleAlign, ';" class="flotr-axis-title flotr-axis-title-x1">', a.x.options.title, '</div>'
);
// Add x2 axis title
if (a.x2.options.title && a.x2.used)
html.push(
'<div style="position:absolute;top:0;left:', this.plotOffset.left, 'px;width:',
this.plotWidth, 'px;text-align:', a.x2.options.titleAlign, ';" class="flotr-axis-title flotr-axis-title-x2">', a.x2.options.title, '</div>'
);
// Add y axis title
if (a.y.options.title && a.y.used)
html.push(
'<div style="position:absolute;top:',
(this.plotOffset.top + this.plotHeight/2 - a.y.titleSize.height/2),
'px;left:0;text-align:', a.y.options.titleAlign, ';" class="flotr-axis-title flotr-axis-title-y1">', a.y.options.title, '</div>'
);
// Add y2 axis title
if (a.y2.options.title && a.y2.used)
html.push(
'<div style="position:absolute;top:',
(this.plotOffset.top + this.plotHeight/2 - a.y.titleSize.height/2),
'px;right:0;text-align:', a.y2.options.titleAlign, ';" class="flotr-axis-title flotr-axis-title-y2">', a.y2.options.title, '</div>'
);
html = html.join('');
var div = D.create('div');
D.setStyles({
color: options.grid.color
});
div.className = 'flotr-titles';
D.insert(this.el, div);
D.insert(div, html);
}
}
});
})();
return Flotr;
}));
@fiznool
Copy link
Author

fiznool commented Jul 3, 2012

To use (with RequireJS 2.0):

// config.js
require.config({
  paths: {
    'underscore' : 'path/to/libs/underscore-1.3.1',
    'bean' : 'path/to/libs/bean-0.4.11-1',
    'flotr2' : 'path/to/libs/flotr2-amd'
  },

  // Needed as Underscore is not AMD
  shim: {
    'underscore' : { 
      exports : '_'
    }
  }
});

// main.js
define(['flotr2'], function(Flotr2) {
  Flotr2.doStuff();
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment