diff --git a/flotr2.amd.js b/flotr2.amd.js
index 1d9fd43a..3c21c618 100644
--- a/flotr2.amd.js
+++ b/flotr2.amd.js
@@ -1,7 +1,7 @@
-(function (root, factory) {
+(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
- define(['bean', 'underscore'], function (bean, _) {
+ 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.
@@ -11,5632 +11,5912 @@
// Browser globals
root.Flotr = factory(root.bean, root._);
}
-}(this, function (bean, _) {
-
-/**
- * Flotr2 (c) 2012 Carl Sutherland
- * MIT License
- * Special thanks to:
- * Flotr: http://code.google.com/p/flotr/ (fork)
- * Flot: https://github.com/flot/flot (original fork)
- */
-(function () {
-
-var
- global = this,
- previousFlotr = this.Flotr,
- Flotr;
-
-Flotr = {
- _: _,
- bean: bean,
- isIphone: /iphone/i.test(navigator.userAgent),
- isIE: (navigator.appVersion.indexOf("MSIE") != -1 ? parseFloat(navigator.appVersion.split("MSIE")[1]) : false),
-
- /**
- * An object of the registered graph types. Use Flotr.addType(type, object)
- * to add your own type.
- */
- graphTypes: {},
-
- /**
- * The list of the registered plugins
- */
- plugins: {},
-
- /**
- * Can be used to add your own chart type.
- * @param {String} name - Type of chart, like 'pies', 'bars' etc.
- * @param {String} graphType - The object containing the basic drawing functions (draw, etc)
- */
- addType: function(name, graphType){
- Flotr.graphTypes[name] = graphType;
- Flotr.defaultOptions[name] = graphType.options || {};
- Flotr.defaultOptions.defaultType = Flotr.defaultOptions.defaultType || name;
- },
-
- /**
- * Can be used to add a plugin
- * @param {String} name - The name of the plugin
- * @param {String} plugin - The object containing the plugin's data (callbacks, options, function1, function2, ...)
- */
- addPlugin: function(name, plugin){
- Flotr.plugins[name] = plugin;
- Flotr.defaultOptions[name] = plugin.options || {};
- },
-
- /**
- * Draws the graph. This function is here for backwards compatibility with Flotr version 0.1.0alpha.
- * You could also draw graphs by directly calling Flotr.Graph(element, data, options).
- * @param {Element} el - element to insert the graph into
- * @param {Object} data - an array or object of dataseries
- * @param {Object} options - an object containing options
- * @param {Class} _GraphKlass_ - (optional) Class to pass the arguments to, defaults to Flotr.Graph
- * @return {Object} returns a new graph object and of course draws the graph.
- */
- draw: function(el, data, options, GraphKlass){
- GraphKlass = GraphKlass || Flotr.Graph;
- return new GraphKlass(el, data, options);
- },
-
- /**
- * Recursively merges two objects.
- * @param {Object} src - source object (likely the object with the least properties)
- * @param {Object} dest - destination object (optional, object with the most properties)
- * @return {Object} recursively merged Object
- * @TODO See if we can't remove this.
- */
- merge: function(src, dest){
- var i, v, result = dest || {};
-
- for (i in src) {
- v = src[i];
- if (v && typeof(v) === 'object') {
- if (v.constructor === Array) {
- result[i] = this._.clone(v);
- } else if (v.constructor !== RegExp && !this._.isElement(v)) {
- result[i] = Flotr.merge(v, (dest ? dest[i] : undefined));
- } else {
- result[i] = v;
- }
- } else {
- result[i] = v;
- }
- }
+}(this, function(bean, _) {
- 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;
- }
- }
+ /**
+ * Flotr2 (c) 2012 Carl Sutherland
+ * MIT License
+ * Special thanks to:
+ * Flotr: http://code.google.com/p/flotr/ (fork)
+ * Flot: https://github.com/flot/flot (original fork)
+ */
+ (function() {
- axis.tickUnit = tickUnit;
- axis.tickSize = tickSize;
+ 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;
+ }
+ };
- var step = tickSize * timeUnits[tickUnit];
- d = new Date(min);
+ global.Flotr = Flotr;
- 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);
+ /**
+ * 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
}
- 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;
- },
+ /**
+ * Flotr Color
+ */
- html : function (text, element, style, className) {
+ (function() {
- var div = D.create('div');
+ 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();
+ }
- D.setStyles(div, { 'position' : 'absolute', 'top' : '-10000px' });
- D.insert(div, '
' + text + '
');
- D.insert(this.o.element, div);
+ // 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;
+ }
+ });
- return D.size(div);
- },
+ Flotr.Color = Color;
- measureText : function (text, style) {
+ })();
- var
- context = this.o.ctx,
- metrics;
+ /**
+ * Flotr Date
+ */
+ Flotr.Date = {
- if (!context.fillText || (F.isIphone && context.measure)) {
- return { width : context.measure(text, style)};
- }
+ set: function(date, name, mode, value) {
+ mode = mode || 'UTC';
+ name = 'set' + (mode === 'UTC' ? 'UTC' : '') + name;
+ date[name](value);
+ },
- 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();
- });
+ get: function(date, name, mode) {
+ mode = mode || 'UTC';
+ name = 'get' + (mode === 'UTC' ? 'UTC' : '') + name;
+ return date[name]();
+ },
- var
- types = _.keys(flotr.graphTypes),
- drawn = false;
+ 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;
+ }
- _.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);
- });
+ var r = [],
+ c,
+ escape = false;
- // 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);
- }
- }
+ for (var i = 0; i < format.length; ++i) {
+ c = format.charAt(i);
- 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;
+ 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);
- 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;
+ // first check global format
+ if (axis.options.timeFormat)
+ return Flotr.Date.format(d, options.timeFormat, options.timeMode);
- 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;
- }
+ var span = (axis.max - axis.min) * scale,
+ t = axis.tickSize * Flotr.Date.timeUnits[axis.tickUnit];
- p.top = Math.floor(p.top); // In order the outline not to be blured
+ 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;
+ }
+ }
- this.plotWidth = this.canvasWidth - p.left - p.right;
- this.plotHeight = this.canvasHeight - p.bottom - p.top;
+ axis.tickUnit = tickUnit;
+ axis.tickSize = tickSize;
- // 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 step = tickSize * timeUnits[tickUnit];
+ d = new Date(min);
- var
- context = this.ctx,
- i;
+ function setTick(name) {
+ set(d, name, mode, Flotr.floorInBase(
+ get(d, name, mode), tickSize
+ ));
+ }
- E.fire(this.el, 'flotr:beforedraw', [this.series, this]);
+ 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;
+ }
- if (this.series.length) {
+ // 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"]
+ };
- context.save();
- context.translate(this.plotOffset.left, this.plotOffset.top);
+ (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
+ };
+ }
+ };
- for (i = 0; i < this.series.length; i++) {
- if (!this.series[i].hide) this.drawSeries(this.series[i]);
- }
+ })();
- context.restore();
- this.clip();
- }
+ /**
+ * 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
+ };
+ }
+ }
+ };
+ })();
- 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);
- }
+ /**
+ * Text Utilities
+ */
+ (function() {
- 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;
- }
+ var
+ F = Flotr,
+ D = F.DOM,
+ _ = F._,
+
+ Text = function(o) {
+ this.o = o;
+ };
+
+ Text.prototype = {
+
+ dimensions: function(text, canvasStyle, htmlStyle, className) {
+
+ if (!text) return {
+ width: 0,
+ height: 0
+ };
+
+ return (this.o.html) ?
+ this.html(text, this.o.element, htmlStyle, className) :
+ this.canvas(text, canvasStyle);
+ },
+
+ canvas: function(text, style) {
+
+ if (!this.o.textEnabled) return;
+ style = style || {};
+
+ var
+ metrics = this.measureText(text, style),
+ width = metrics.width,
+ height = style.size || F.defaultOptions.fontSize,
+ angle = style.angle || 0,
+ cosAngle = Math.cos(angle),
+ sinAngle = Math.sin(angle),
+ widthPadding = 2,
+ heightPadding = 6,
+ bounds;
+
+ bounds = {
+ width: Math.abs(cosAngle * width) + Math.abs(sinAngle * height) + widthPadding,
+ height: Math.abs(sinAngle * width) + Math.abs(cosAngle * height) + heightPadding
+ };
+
+ return bounds;
+ },
+
+ html: function(text, element, style, className) {
+
+ var div = D.create('div');
+
+ D.setStyles(div, {
+ 'position': 'absolute',
+ 'top': '-10000px'
+ });
+ D.insert(div, '' + text + '
');
+ D.insert(this.o.element, div);
+
+ return D.size(div);
+ },
+
+ measureText: function(text, style) {
+
+ var
+ context = this.o.ctx,
+ metrics;
+
+ if (!context.fillText || (F.isIphone && context.measure)) {
+ return {
+ width: context.measure(text, style)
+ };
+ }
+
+ style = _.extend({
+ size: F.defaultOptions.fontSize,
+ weight: 1,
+ angle: 0
+ }, style);
+
+ context.save();
+ context.font = (style.weight > 1 ? "bold " : "") + (style.size * 1.3) + "px sans-serif";
+ metrics = context.measureText(text);
+ context.restore();
+
+ return metrics;
+ }
+ };
- 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);
+ Flotr.Text = Text;
- 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;
+ /**
+ * 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;
}
- E.fire(el, 'flotr:mousedown', [event, this]);
- this.observe(document, 'touchend', touchendHandler);
- }, 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;
- this.observe(this.overlay, 'touchmove', _.bind(function (e) {
+ })();
- var pos = this.getEventPosition(e);
+ /**
+ * Flotr Axis Library
+ */
- if (this.options.preventDefault) {
- e.preventDefault();
- }
+ (function() {
+
+ var
+ _ = Flotr._,
+ LOGARITHMIC = 'logarithmic';
+
+ function Axis(o) {
- movement = true;
+ this.orientation = 1;
+ this.offset = 0;
+ this.datamin = Number.MAX_VALUE;
+ this.datamax = -Number.MAX_VALUE;
- if (this.multitouches || (e.touches && e.touches.length > 1)) {
- this.multitouches = e.touches;
- } else {
- if (!touchend) {
- E.fire(el, 'flotr:mousemove', [event, pos, this]);
- }
+ _.extend(this, o);
}
- 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;
+ // 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
+ })
+ });
+ }
+ }
+ }
- if(size.width <= 0 || size.height <= 0 || o.resolution <= 0){
- throw 'Invalid dimensions for plot, width = ' + size.width + ', height = ' + size.height + ', resolution = ' + o.resolution;
- }
+ }
+ };
+
+
+ // 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
+ })
+ };
+ }
+ });
- // 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;
- }
+ // Helper Methods
- 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);
+ function log(value, base) {
+ value = Math.log(Math.max(value, Number.MIN_VALUE));
+ if (base !== Math.E)
+ value /= Math.log(base);
+ return value;
+ }
- // 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]);
+ function exp(value, base) {
+ return (base === Math.E) ? Math.exp(value) : Math.pow(base, value);
+ }
- // 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);
+ Flotr.Axis = Axis;
- /**
- * @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;
- }
- }
+ /**
+ * Flotr Series Library
+ */
- // 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;
- }
- },
+ (function() {
- _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';
+ var
+ _ = Flotr._;
- el.graph = this;
- this.el = el;
- }
-};
+ function Series(o) {
+ _.extend(this, o);
+ }
-Flotr.Graph = Graph;
+ Series.prototype = {
+
+ getRange: function() {
+
+ var
+ data = this.data,
+ length = data.length,
+ xmin = Number.MAX_VALUE,
+ ymin = Number.MAX_VALUE,
+ xmax = -Number.MAX_VALUE,
+ ymax = -Number.MAX_VALUE,
+ xused = false,
+ yused = false,
+ x, y, i;
+
+ if (length < 0 || this.hide) return false;
+
+ for (i = 0; i < length; i++) {
+ x = data[i][0];
+ y = data[i][1];
+ if (x !== null) {
+ if (x < xmin) {
+ xmin = x;
+ xused = true;
+ }
+ if (x > xmax) {
+ xmax = x;
+ xused = true;
+ }
+ }
+ if (y !== null) {
+ if (y < ymin) {
+ ymin = y;
+ yused = true;
+ }
+ if (y > ymax) {
+ ymax = y;
+ yused = true;
+ }
+ }
+ }
+
+ return {
+ xmin: xmin,
+ xmax: xmax,
+ ymin: ymin,
+ ymax: ymax,
+ xused: xused,
+ yused: yused
+ };
+ }
+ };
+
+ _.extend(Series, {
+ /**
+ * Collects dataseries from input and parses the series into the right format. It returns an Array
+ * of Objects each having at least the 'data' 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
+ },
-/**
- * Flotr Axis Library
- */
+ stack: {
+ values: []
+ },
-(function () {
+ /**
+ * Draws lines series in the canvas element.
+ * @param {Object} options
+ */
+ draw: function(options) {
-var
- _ = Flotr._,
- LOGARITHMIC = 'logarithmic';
+ var
+ context = options.context,
+ lineWidth = options.lineWidth,
+ shadowSize = options.shadowSize,
+ offset;
-function Axis (o) {
+ context.save();
+ context.lineJoin = 'round';
- this.orientation = 1;
- this.offset = 0;
- this.datamin = Number.MAX_VALUE;
- this.datamax = -Number.MAX_VALUE;
+ if (shadowSize) {
- _.extend(this, o);
-}
+ 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);
-// Prototype
-Axis.prototype = {
+ context.strokeStyle = "rgba(0,0,0,0.2)";
+ this.plot(options, offset, false);
+ }
- 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;
+ context.lineWidth = lineWidth;
+ context.strokeStyle = options.color;
- 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();
- }
- }
+ this.plot(options, 0, true);
- // 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;
- }
+ context.restore();
+ },
- if (max == min) {
- var widen = max ? 0.01 : 1.00;
- if (o.min === null) min -= widen;
- if (o.max === null) max += widen;
- }
+ 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;
- 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);
- }
+ context.beginPath();
- axis.min = min;
- axis.max = max; //extendRange may use axis.min or axis.max, so it should be set before it is caled
+ 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);
+ }
+ }
- // 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 (!options.fill || options.fill && !options.fillBorder) context.stroke();
- if (axis.min == axis.max) axis.max = axis.min + 1;
- },
+ fill();
- calculateTextDimensions : function (T, options) {
+ 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();
+ }
+ }
+ }
- var maxLabel = '',
- length,
- i;
+ context.closePath();
+ },
- 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;
- }
- }
- }
+ // 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;
+ }
- 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}) });
+ 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.
+ },
-// 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})
- };
- }
-});
+ stack: {
+ positive: [],
+ negative: [],
+ _positive: [], // Shadow
+ _negative: [] // Shadow
+ },
+ draw: function(options) {
+ var
+ context = options.context;
-// Helper Methods
+ 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;
-function log (value, base) {
- value = Math.log(Math.max(value, Number.MIN_VALUE));
- if (base !== Math.E)
- value /= Math.log(base);
- return value;
-}
+ this.plot(options);
-function exp (value, base) {
- return (base === Math.E) ? Math.exp(value) : Math.pow(base, value);
-}
+ context.restore();
+ },
-Flotr.Axis = Axis;
+ plot: function(options) {
-})();
+ var
+ data = options.data,
+ context = options.context,
+ shadowSize = options.shadowSize,
+ i, geometry, left, top, width, height;
-/**
- * Flotr Series Library
- */
+ if (data.length < 1) return;
-(function () {
+ this.translate(context, options.horizontal);
-var
- _ = Flotr._;
+ for (i = 0; i < data.length; i++) {
-function Series (o) {
- _.extend(this, o);
-}
+ geometry = this.getBarGeometry(data[i][0], data[i][1], options);
+ if (geometry === null) continue;
-Series.prototype = {
+ left = geometry.left;
+ top = geometry.top;
+ width = geometry.width;
+ height = geometry.height;
- getRange: function () {
+ 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);
+ }
+ }
+ },
- 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;
+ translate: function(context, horizontal) {
+ if (horizontal) {
+ context.rotate(-Math.PI / 2);
+ context.scale(-1, 1);
+ }
+ },
- if (length < 0 || this.hide) return false;
+ 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;
+ }
- for (i = 0; i < length; i++) {
- x = data[i][0];
- y = data[i][1];
- if (x !== null) {
- if (x < xmin) { xmin = x; xused = true; }
- if (x > xmax) { xmax = x; xused = true; }
- }
- if (y !== null) {
- if (y < ymin) { ymin = y; yused = true; }
- if (y > ymax) { ymax = y; yused = true; }
- }
- }
+ // Stacked bars
+ if (stack) {
+ stackValue = yValue > 0 ? stack.positive : stack.negative;
+ stackOffset = stackValue[xValue] || stackOffset;
+ stackValue[xValue] = stackOffset + yValue;
+ }
- 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);
- }
+ 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
+ };
+ },
- 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]) {
+ 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();
- 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]);
- }
+ context.restore();
+ },
- // Negative
- else {
- negativeSums[x] = (negativeSums[x] || 0) + data[j][1];
- newmin = Math.min(newmin, negativeSums[x]);
- }
- }
+ 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();
+ },
- lines.negativeSums = negativeSums;
- lines.positiveSums = positiveSums;
+ extendXRange: function(axis, data, options, bars) {
+ this._extendRange(axis, data, options, bars);
+ this.groups = (this.groups + 1) || 1;
+ this.current = 0;
+ },
- axis.max = newmax;
- axis.min = newmin;
- }
+ 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.steps) {
+ 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]);
+ }
+ }
+ }
- 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;
+ // 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);
+ }
}
- 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.stackMin = newmin;
+ this.stackMax = newmax;
+ this.negativeSums = negativeSums;
+ this.positiveSums = positiveSums;
- 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);
+ axis.max = newmax;
+ axis.min = newmin;
}
- };
- }
- }
-
-});
-
-/** 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;
- }
+ /** 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) {
- 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);
- }
- }
+ var
+ data = options.data,
+ context = options.context,
+ geometry,
+ i, x, y, z;
- if (options.stacked &&
- ((orientation == 1 && horizontal) || (orientation == -1 && !horizontal))){
+ offset = offset || 0;
- for (j = data.length; j--;) {
- value = data[j][(orientation == 1 ? 1 : 0)]+'';
- datum = data[j][(orientation == 1 ? 0 : 1)];
+ for (i = 0; i < data.length; ++i) {
- // Positive
- if (datum > 0) {
- positiveSums[value] = (positiveSums[value] || 0) + datum;
- newmax = Math.max(newmax, positiveSums[value]);
- }
+ geometry = this.getGeometry(data[i], options);
- // Negative
- else {
- negativeSums[value] = (negativeSums[value] || 0) + datum;
- newmin = Math.min(newmin, negativeSums[value]);
- }
- }
- }
+ 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) {
- // 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);
- }
- }
+ var
+ context = options.context,
+ geometry = this.getGeometry(options.data[options.args.index], options);
- 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';
+ 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)
+ });
- /**
- * @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;
- }
+ /** 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();
+ }
}
- for (j in stackedSumsPos) {
- newmax = Math.max(stackedSumsPos[j], newmax);
+ },
+ 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);
}
}
- }
- 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;
- }
- }
+ /** 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
+ });
+ }
- 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.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;
- 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
- };
+ var i, y, s, d,
+ xa = series.xaxis,
+ ya = series.yaxis,
+ ctx = this.ctx,
+ sw = this.options.shadowSize;
- 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('', label, '
');
- }
- else {
- style.textAlign = textAlign;
- style.textBaseline = textBaseline;
- Flotr.drawText(context, label, distX, distY, style);
- }
- }
-
- if (options.htmlText || !options.textEnabled) {
- var div = Flotr.DOM.node('
');
- 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;
- }
+ for (i = 0; i < data.length; i++) {
+ y = data[i][0];
+ s = data[i][1];
+ d = data[i][2];
- 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);
- }
+ if (s === null || d === null) continue;
- 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);
+ 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);
+ }
+ }
});
- },
- extendRange : function (series) {
+ /** 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
+ });
+ }
+ });
- var
- data = series.data,
- xa = series.xaxis,
- ya = series.yaxis,
- w = series.timeline.barWidth;
+ function isImage(i) {
+ return typeof i === 'object' && i.constructor && (Image ? true : i.constructor === Image);
+ }
- if (xa.options.min === null)
- xa.min = xa.datamin - w / 2;
+ })();
- if (xa.options.max === null) {
+ /**
+ * Pie
+ *
+ * Formats the pies labels.
+ * @param {Object} slice - Slice object
+ * @return {String} Formatted pie label string
+ */
+ (function() {
- var
- max = xa.max;
+ 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('', label, '
');
+ } else {
+ style.textAlign = textAlign;
+ style.textBaseline = textBaseline;
+ Flotr.drawText(context, label, distX, distY, style);
+ }
+ }
+
+ if (options.htmlText || !options.textEnabled) {
+ var div = Flotr.DOM.node('
');
+ 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
+ },
- Flotr._.each(data, function (timeline) {
- max = Math.max(max, timeline[0] + timeline[2]);
- }, this);
+ draw: function(options) {
+ var
+ context = options.context,
+ lineWidth = options.lineWidth,
+ shadowSize = options.shadowSize;
- xa.max = max + w / 2;
- }
+ context.save();
- 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) + 0.5,
- y = plotOffset.top + Math.round(pos.relY) + 0.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 = ''+this.canvas.firstChild.innerHTML+'';
- return window.open().document.write(image);
- }
+ if (shadowSize > 0) {
+ context.lineWidth = shadowSize / 2;
- if (type !== 'jpeg' && type !== 'png') return;
+ context.strokeStyle = 'rgba(0,0,0,0.1)';
+ this.plot(options, shadowSize / 2 + context.lineWidth / 2);
- image = getImage(type, this.canvas, width, height);
+ context.strokeStyle = 'rgba(0,0,0,0.2)';
+ this.plot(options, context.lineWidth / 2);
+ }
- 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);
- }
- },
+ context.lineWidth = options.lineWidth;
+ context.strokeStyle = options.color;
+ if (options.fill) context.fillStyle = options.fillStyle;
- restoreCanvas: function() {
- D.show(this.canvas);
- D.show(this.overlay);
- if (this.saveImageElement) this.el.removeChild(this.saveImageElement);
- this.saveImageElement = null;
- }
-});
+ 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();
+ }
+ }
+ });
-(function () {
+ /** 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;
-var E = Flotr.EventAdapter,
- _ = Flotr._;
+ offset = offset || 0;
-Flotr.addPlugin('graphGrid', {
+ context.beginPath();
+ for (i = 0; i < data.length; ++i) {
+ ratio = data[i][1] / this.max;
- 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
- );
+ 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);
}
- }
- }
- 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();
+ Flotr.addType('timeline', {
+ options: {
+ show: false,
+ lineWidth: 1,
+ barWidth: 0.2,
+ fill: true,
+ fillColor: null,
+ fillOpacity: 0.4,
+ centered: true
+ },
- if (backgroundImage) {
+ draw: function(options) {
- src = backgroundImage.src || backgroundImage;
- left = (parseInt(backgroundImage.left, 10) || 0) + plotOffset.left;
- top = (parseInt(backgroundImage.top, 10) || 0) + plotOffset.top;
- img = new Image();
+ var
+ context = options.context;
- 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();
- };
+ context.save();
+ context.lineJoin = 'miter';
+ context.lineWidth = options.lineWidth;
+ context.strokeStyle = options.color;
+ context.fillStyle = options.fillStyle;
- 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) {
- var
- hit = this.hit.track(pos);
- _.defaults(pos, hit);
- },
- '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;})) {
- return 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
- };
+ this.plot(options);
- 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 {
+ context.restore();
+ },
- 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;
- }
- }
- }
+ plot: function(options) {
- 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]);
- }
- }
+ var
+ data = options.data,
+ context = options.context,
+ xScale = options.xScale,
+ yScale = options.yScale,
+ barWidth = options.barWidth,
+ lineWidth = options.lineWidth,
+ i;
- return n;
- },
-
- 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;
- }
+ Flotr._.each(data, function(timeline) {
- for (i = 0; i < series.length; i++) {
+ var
+ x = timeline[0],
+ y = timeline[1],
+ w = timeline[2],
+ h = barWidth,
- serie = series[i];
- data = serie.data;
- mouseX = serie.xaxis.p2d(relX);
- mouseY = serie.yaxis.p2d(relY);
+ xt = Math.ceil(xScale(x)),
+ wt = Math.ceil(xScale(x + w)) - xt,
+ yt = Math.round(yScale(y)),
+ ht = Math.round(yScale(y - h)) - yt,
- for (j = data.length; j--;) {
+ x0 = xt - lineWidth / 2,
+ y0 = Math.round(yt - ht / 2) - lineWidth / 2;
- x = data[j][0];
- y = data[j][1];
+ context.strokeRect(x0, y0, wt, ht);
+ context.fillRect(x0, y0, wt, ht);
- 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;
+ extendRange: function(series) {
- distanceX = Math.abs(x - mouseX);
- distanceY = Math.abs(y - mouseY);
+ var
+ data = series.data,
+ xa = series.xaxis,
+ ya = series.yaxis,
+ w = series.timeline.barWidth;
- // Skip square root for speed
- distance = distanceX * distanceX + distanceY * distanceY;
+ if (xa.options.min === null)
+ xa.min = xa.datamin - w / 2;
- if (distance < compare) {
- compare = distance;
- setClosest(closest);
- }
+ if (xa.options.max === null) {
- 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('
');
- this.mouseTrack = mouseTrack;
- D.insert(this.el, mouseTrack);
- }
+ var
+ max = xa.max;
- if (!n.mouse.relative) { // absolute to the canvas
+ Flotr._.each(data, function(timeline) {
+ max = Math.max(max, timeline[0] + timeline[2]);
+ }, this);
- 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;';
+ xa.max = max + w / 2;
+ }
- // 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 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,
- a = this.axes,
- first = s.first,
- second = s.second,
- x1, x2, y1, y2;
-
- 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);
-
- return {
- 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: {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
- area = this.selection.getArea();
- name = name || 'select';
- area.selection = this.selection.selection;
- E.fire(this.el, 'flotr:'+name, [area, 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);
- }
+ (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) + 0.5,
+ y = plotOffset.top + Math.round(pos.relY) + 0.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
+ );
+ }
+ }
+ });
+ })();
- 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;
- }
+ (function() {
- 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});
+ 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;
+ }
- this.selection.prevSelection = {
- first: { x: s.first.x, y: s.first.y },
- second: { x: s.second.x, y: s.second.y }
- };
+ Flotr.addPlugin('download', {
+
+ saveImage: function(type, width, height, replaceCanvas) {
+ var image = null;
+ if (Flotr.isIE && Flotr.isIE < 9) {
+ image = '' + this.canvas.firstChild.innerHTML + '';
+ 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;
+ }
+ });
- 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);
- }
+ })();
+
+ (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;
+ }
+ }
+ });
- 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() {
- 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
- );
- }
- }
+ 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) {
+ var
+ hit = this.hit.track(pos);
+ _.defaults(pos, hit);
+ },
+ '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;
+ })) {
+ return 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]);
+ }
+ }
+
+ return n;
+ },
+
+ 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('
');
+ 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;';
+
+ // 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 (/w/.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';
+ }
+ }
+ }
- 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);
- }
- }
+ });
+ })();
+
+ /**
+ * Selection Handles Plugin
+ *
+ *
+ * Options
+ * show - True enables the handles plugin.
+ * drag - Left and Right drag handles
+ * scroll - Scrolling handle
+ */
+ (function() {
- 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 += [
- '' + tick.label + '
'
- ].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,
- 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,
- opacity = legend.backgroundOpacity,
- i, label, color;
-
- if (itemCount) {
-
- var lbw = legend.labelBoxWidth,
- lbh = legend.labelBoxHeight,
- lbm = legend.labelBoxMargin,
- offsetX = plotOffset.left + m,
- offsetY = plotOffset.top + m,
- labelMaxWidth = 0,
- style = {
- size: options.fontSize*1.1,
- color: options.grid.color
- };
-
- // We calculate the labels' max width
- 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);
-
- // Default Opacity
- if (!opacity && opacity !== 0) {
- opacity = 0.1;
- }
-
- if (!options.HtmlText && this.textEnabled && !legend.container) {
-
- if(p.charAt(0) == 's') offsetY = plotOffset.top + this.plotHeight - (m + legendHeight);
- if(p.charAt(0) == 'c') offsetY = plotOffset.top + (this.plotHeight/2) - (m + (legendHeight/2));
- if(p.charAt(1) == 'e') offsetX = plotOffset.left + this.plotWidth - (m + legendWidth);
-
- // Legend box
- color = this.processColor(legend.backgroundColor, { opacity : opacity });
-
- 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 ? '' : ' ');
- rowStarted = true;
- }
-
- var s = series[i],
- boxWidth = legend.labelBoxWidth,
- boxHeight = legend.labelBoxHeight;
-
- 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(
- '',
- '',
- '
', // Border
- '
', // Background
- '
',
- '
',
- ' ',
- '', label, ' '
- );
- }
- if(rowStarted) fragments.push(' ');
-
- if(fragments.length > 0){
- var table = '' + fragments.join('') + '
';
- 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) == 'c') { styles.top = (m + (this.plotHeight - legendHeight) / 2) + '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 (!opacity) 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, opacity);
- D.insert(div, ' ');
- D.insert(this.el, div);
- }
+ function isLeftClick(e, type) {
+ return (e.which ? (e.which === 1) : (e.button === 0 || e.button === 1));
}
- }
- }
- }
-});
-})();
-
-/** 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('
'),
- graph = D.node(''+this.options.spreadsheet.tabGraphLabel+'
'),
- data = D.node(''+this.options.spreadsheet.tabDataLabel+'
'),
- 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;
+
+ function boundX(x, graph) {
+ return Math.min(Math.max(0, x), graph.plotWidth - 1);
}
- });
- });
- // 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 = [' '],
- buttonDownload, buttonSelect, t;
-
- // First row : series' labels
- var html = [''];
- html.push(' ');
- _.each(s, function(serie,i){
- html.push(''+(serie.label || String.fromCharCode(65+i))+' ');
- colgroup.push(' ');
- });
- html.push(' ');
- // Data rows
- _.each(datagrid, function(row){
- html.push('');
- _.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;
+ function boundY(y, graph) {
+ return Math.min(Math.max(0, y), graph.plotHeight);
}
- html.push('<'+tag+(tag=='th'?' scope="row"':'')+'>'+content+''+tag+'>');
- }, this);
- html.push(' ');
- }, this);
- colgroup.push('');
- t = D.node(html.join(''));
+ 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,
+ a = this.axes,
+ first = s.first,
+ second = s.second,
+ x1, x2, y1, y2;
+
+ 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);
+
+ return {
+ 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: {
+ 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
+ area = this.selection.getArea();
+ name = name || 'select';
+ area.selection = this.selection.selection;
+ E.fire(this.el, 'flotr:' + name, [area, 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;
+ }
+
+ });
- /**
- * @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(
- '' +
- this.options.spreadsheet.toolbarDownload +
- ' ');
-
- buttonSelect = D.node(
- '' +
- this.options.spreadsheet.toolbarSelectAll+
- ' ');
-
- this.
- observe(buttonDownload, 'click', _.bind(this.spreadsheet.downloadCSV, this)).
- observe(buttonSelect, 'click', _.bind(this.spreadsheet.selectAllData, this));
-
- var toolbar = D.node('
');
- D.insert(toolbar, buttonDownload);
- D.insert(toolbar, buttonSelect);
-
- var containerHeight =this.canvasHeight - D.size(this.spreadsheet.tabsContainer).height-2,
- container = D.node('
');
-
- 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, '\\"')+'"';
- });
+ })();
+
+ (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 += [
+ '' + tick.label + '
'
+ ].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));
+ }
+ }
+ }
+ }
+ }
- 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 () {
+ (function() {
-var D = Flotr.DOM;
+ 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,
+ 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,
+ opacity = legend.backgroundOpacity,
+ i, label, color;
+
+ if (itemCount) {
+
+ var lbw = legend.labelBoxWidth,
+ lbh = legend.labelBoxHeight,
+ lbm = legend.labelBoxMargin,
+ offsetX = plotOffset.left + m,
+ offsetY = plotOffset.top + m,
+ labelMaxWidth = 0,
+ style = {
+ size: options.fontSize * 1.1,
+ color: options.grid.color
+ };
+
+ // We calculate the labels' max width
+ 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);
+
+ // Default Opacity
+ if (!opacity && opacity !== 0) {
+ opacity = 0.1;
+ }
+
+ if (!options.HtmlText && this.textEnabled && !legend.container) {
+
+ if (p.charAt(0) == 's') offsetY = plotOffset.top + this.plotHeight - (m + legendHeight);
+ if (p.charAt(0) == 'c') offsetY = plotOffset.top + (this.plotHeight / 2) - (m + (legendHeight / 2));
+ if (p.charAt(1) == 'e') offsetX = plotOffset.left + this.plotWidth - (m + legendWidth);
+
+ // Legend box
+ color = this.processColor(legend.backgroundColor, {
+ opacity: opacity
+ });
+
+ 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 ? '' : ' ');
+ rowStarted = true;
+ }
+
+ var s = series[i],
+ boxWidth = legend.labelBoxWidth,
+ boxHeight = legend.labelBoxHeight;
+
+ 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(
+ '',
+ '',
+ '
', // Border
+ '
', // Background
+ '
',
+ '
',
+ ' ',
+ '', label, ' '
+ );
+ }
+ if (rowStarted) fragments.push(' ');
+
+ if (fragments.length > 0) {
+ var table = '' + fragments.join('') + '
';
+ 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) == 'c') {
+ styles.top = (m + (this.plotHeight - legendHeight) / 2) + '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 (!opacity) 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, opacity);
+ 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;
+ }
+ }
-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(
- '', options.title, '
'
- );
-
- // Add subtitle
- if (options.subtitle)
- html.push(
- '', options.subtitle, '
'
- );
-
- html.push('');
-
- html.push('');
-
- // Add x axis title
- if (a.x.options.title && a.x.used)
- html.push(
- '
', a.x.options.title, '
'
- );
-
- // Add x2 axis title
- if (a.x2.options.title && a.x2.used)
- html.push(
- '
', a.x2.options.title, '
'
- );
-
- // Add y axis title
- if (a.y.options.title && a.y.used)
- html.push(
- '
', a.y.options.title, '
'
- );
-
- // Add y2 axis title
- if (a.y2.options.title && a.y2.used)
- html.push(
- '
', a.y2.options.title, '
'
- );
-
- 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);
- }
- }
-});
-})();
+ 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('
'),
+ graph = D.node('
' + this.options.spreadsheet.tabGraphLabel + '
'),
+ data = D.node('
' + this.options.spreadsheet.tabDataLabel + '
'),
+ 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 = ['
'],
+ buttonDownload, buttonSelect, t;
+
+ // First row : series' labels
+ var html = [''];
+ html.push(' ');
+ _.each(s, function(serie, i) {
+ html.push('' + (serie.label || String.fromCharCode(65 + i)) + ' ');
+ colgroup.push(' ');
+ });
+ html.push(' ');
+ // Data rows
+ _.each(datagrid, function(row) {
+ html.push('');
+ _.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(' ');
+ }, this);
+ colgroup.push('');
+ 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(
+ '' +
+ this.options.spreadsheet.toolbarDownload +
+ ' ');
+
+ buttonSelect = D.node(
+ '' +
+ this.options.spreadsheet.toolbarSelectAll +
+ ' ');
+
+ this.
+ observe(buttonDownload, 'click', _.bind(this.spreadsheet.downloadCSV, this)).
+ observe(buttonSelect, 'click', _.bind(this.spreadsheet.selectAllData, this));
+
+ var toolbar = D.node('
');
+ D.insert(toolbar, buttonDownload);
+ D.insert(toolbar, buttonSelect);
+
+ var containerHeight = this.canvasHeight - D.size(this.spreadsheet.tabsContainer).height - 2,
+ container = D.node('
');
+
+ 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(
+ '', options.title, '
'
+ );
+
+ // Add subtitle
+ if (options.subtitle)
+ html.push(
+ '', options.subtitle, '
'
+ );
+
+ html.push('');
+
+ html.push('');
+
+ // Add x axis title
+ if (a.x.options.title && a.x.used)
+ html.push(
+ '
', a.x.options.title, '
'
+ );
+
+ // Add x2 axis title
+ if (a.x2.options.title && a.x2.used)
+ html.push(
+ '
', a.x2.options.title, '
'
+ );
+
+ // Add y axis title
+ if (a.y.options.title && a.y.used)
+ html.push(
+ '
', a.y.options.title, '
'
+ );
+
+ // Add y2 axis title
+ if (a.y2.options.title && a.y2.used)
+ html.push(
+ '
', a.y2.options.title, '
'
+ );
+
+ 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;
+ return Flotr;
-}));
\ No newline at end of file
+}));