META-INF.resources.lifebuoy.envision.js Maven / Gradle / Ivy
// Underscore.js 1.1.7
// (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.
// Underscore is freely distributable under the MIT license.
// Portions of Underscore are inspired or borrowed from Prototype,
// Oliver Steele's Functional, and John Resig's Micro-Templating.
// For all details and documentation:
// http://documentcloud.github.com/underscore
(function() {
// Baseline setup
// --------------
// Establish the root object, `window` in the browser, or `global` on the server.
var root = this;
// Save the previous value of the `_` variable.
var previousUnderscore = root._;
// Establish the object that gets returned to break out of a loop iteration.
var breaker = {};
// Save bytes in the minified (but not gzipped) version:
var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
// Create quick reference variables for speed access to core prototypes.
var slice = ArrayProto.slice,
unshift = ArrayProto.unshift,
toString = ObjProto.toString,
hasOwnProperty = ObjProto.hasOwnProperty;
// All **ECMAScript 5** native function implementations that we hope to use
// are declared here.
var
nativeForEach = ArrayProto.forEach,
nativeMap = ArrayProto.map,
nativeReduce = ArrayProto.reduce,
nativeReduceRight = ArrayProto.reduceRight,
nativeFilter = ArrayProto.filter,
nativeEvery = ArrayProto.every,
nativeSome = ArrayProto.some,
nativeIndexOf = ArrayProto.indexOf,
nativeLastIndexOf = ArrayProto.lastIndexOf,
nativeIsArray = Array.isArray,
nativeKeys = Object.keys,
nativeBind = FuncProto.bind;
// Create a safe reference to the Underscore object for use below.
var _ = function(obj) { return new wrapper(obj); };
// Export the Underscore object for **CommonJS**, with backwards-compatibility
// for the old `require()` API. If we're not in CommonJS, add `_` to the
// global object.
if (typeof module !== 'undefined' && module.exports) {
module.exports = _;
_._ = _;
} else {
// Exported as a string, for Closure Compiler "advanced" mode.
root['_'] = _;
}
// Current version.
_.VERSION = '1.1.7';
// Collection Functions
// --------------------
// The cornerstone, an `each` implementation, aka `forEach`.
// Handles objects with the built-in `forEach`, arrays, and raw objects.
// Delegates to **ECMAScript 5**'s native `forEach` if available.
var each = _.each = _.forEach = function(obj, iterator, context) {
if (obj == null) return;
if (nativeForEach && obj.forEach === nativeForEach) {
obj.forEach(iterator, context);
} else if (obj.length === +obj.length) {
for (var i = 0, l = obj.length; i < l; i++) {
if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return;
}
} else {
for (var key in obj) {
if (hasOwnProperty.call(obj, key)) {
if (iterator.call(context, obj[key], key, obj) === breaker) return;
}
}
}
};
// Return the results of applying the iterator to each element.
// Delegates to **ECMAScript 5**'s native `map` if available.
_.map = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
each(obj, function(value, index, list) {
results[results.length] = iterator.call(context, value, index, list);
});
return results;
};
// **Reduce** builds up a single result from a list of values, aka `inject`,
// or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
_.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
var initial = memo !== void 0;
if (obj == null) obj = [];
if (nativeReduce && obj.reduce === nativeReduce) {
if (context) iterator = _.bind(iterator, context);
return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
}
each(obj, function(value, index, list) {
if (!initial) {
memo = value;
initial = true;
} else {
memo = iterator.call(context, memo, value, index, list);
}
});
if (!initial) throw new TypeError("Reduce of empty array with no initial value");
return memo;
};
// The right-associative version of reduce, also known as `foldr`.
// Delegates to **ECMAScript 5**'s native `reduceRight` if available.
_.reduceRight = _.foldr = function(obj, iterator, memo, context) {
if (obj == null) obj = [];
if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
if (context) iterator = _.bind(iterator, context);
return memo !== void 0 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
}
var reversed = (_.isArray(obj) ? obj.slice() : _.toArray(obj)).reverse();
return _.reduce(reversed, iterator, memo, context);
};
// Return the first value which passes a truth test. Aliased as `detect`.
_.find = _.detect = function(obj, iterator, context) {
var result;
any(obj, function(value, index, list) {
if (iterator.call(context, value, index, list)) {
result = value;
return true;
}
});
return result;
};
// Return all the elements that pass a truth test.
// Delegates to **ECMAScript 5**'s native `filter` if available.
// Aliased as `select`.
_.filter = _.select = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
each(obj, function(value, index, list) {
if (iterator.call(context, value, index, list)) results[results.length] = value;
});
return results;
};
// Return all the elements for which a truth test fails.
_.reject = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
each(obj, function(value, index, list) {
if (!iterator.call(context, value, index, list)) results[results.length] = value;
});
return results;
};
// Determine whether all of the elements match a truth test.
// Delegates to **ECMAScript 5**'s native `every` if available.
// Aliased as `all`.
_.every = _.all = function(obj, iterator, context) {
var result = true;
if (obj == null) return result;
if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
each(obj, function(value, index, list) {
if (!(result = result && iterator.call(context, value, index, list))) return breaker;
});
return result;
};
// Determine if at least one element in the object matches a truth test.
// Delegates to **ECMAScript 5**'s native `some` if available.
// Aliased as `any`.
var any = _.some = _.any = function(obj, iterator, context) {
iterator = iterator || _.identity;
var result = false;
if (obj == null) return result;
if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
each(obj, function(value, index, list) {
if (result |= iterator.call(context, value, index, list)) return breaker;
});
return !!result;
};
// Determine if a given value is included in the array or object using `===`.
// Aliased as `contains`.
_.include = _.contains = function(obj, target) {
var found = false;
if (obj == null) return found;
if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
any(obj, function(value) {
if (found = value === target) return true;
});
return found;
};
// Invoke a method (with arguments) on every item in a collection.
_.invoke = function(obj, method) {
var args = slice.call(arguments, 2);
return _.map(obj, function(value) {
return (method.call ? method || value : value[method]).apply(value, args);
});
};
// Convenience version of a common use case of `map`: fetching a property.
_.pluck = function(obj, key) {
return _.map(obj, function(value){ return value[key]; });
};
// Return the maximum element or (element-based computation).
_.max = function(obj, iterator, context) {
if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj);
var result = {computed : -Infinity};
each(obj, function(value, index, list) {
var computed = iterator ? iterator.call(context, value, index, list) : value;
computed >= result.computed && (result = {value : value, computed : computed});
});
return result.value;
};
// Return the minimum element (or element-based computation).
_.min = function(obj, iterator, context) {
if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj);
var result = {computed : Infinity};
each(obj, function(value, index, list) {
var computed = iterator ? iterator.call(context, value, index, list) : value;
computed < result.computed && (result = {value : value, computed : computed});
});
return result.value;
};
// Sort the object's values by a criterion produced by an iterator.
_.sortBy = function(obj, iterator, context) {
return _.pluck(_.map(obj, function(value, index, list) {
return {
value : value,
criteria : iterator.call(context, value, index, list)
};
}).sort(function(left, right) {
var a = left.criteria, b = right.criteria;
return a < b ? -1 : a > b ? 1 : 0;
}), 'value');
};
// Groups the object's values by a criterion produced by an iterator
_.groupBy = function(obj, iterator) {
var result = {};
each(obj, function(value, index) {
var key = iterator(value, index);
(result[key] || (result[key] = [])).push(value);
});
return result;
};
// Use a comparator function to figure out at what index an object should
// be inserted so as to maintain order. Uses binary search.
_.sortedIndex = function(array, obj, iterator) {
iterator || (iterator = _.identity);
var low = 0, high = array.length;
while (low < high) {
var mid = (low + high) >> 1;
iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid;
}
return low;
};
// Safely convert anything iterable into a real, live array.
_.toArray = function(iterable) {
if (!iterable) return [];
if (iterable.toArray) return iterable.toArray();
if (_.isArray(iterable)) return slice.call(iterable);
if (_.isArguments(iterable)) return slice.call(iterable);
return _.values(iterable);
};
// Return the number of elements in an object.
_.size = function(obj) {
return _.toArray(obj).length;
};
// Array Functions
// ---------------
// Get the first element of an array. Passing **n** will return the first N
// values in the array. Aliased as `head`. The **guard** check allows it to work
// with `_.map`.
_.first = _.head = function(array, n, guard) {
return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
};
// Returns everything but the first entry of the array. Aliased as `tail`.
// Especially useful on the arguments object. Passing an **index** will return
// the rest of the values in the array from that index onward. The **guard**
// check allows it to work with `_.map`.
_.rest = _.tail = function(array, index, guard) {
return slice.call(array, (index == null) || guard ? 1 : index);
};
// Get the last element of an array.
_.last = function(array) {
return array[array.length - 1];
};
// Trim out all falsy values from an array.
_.compact = function(array) {
return _.filter(array, function(value){ return !!value; });
};
// Return a completely flattened version of an array.
_.flatten = function(array) {
return _.reduce(array, function(memo, value) {
if (_.isArray(value)) return memo.concat(_.flatten(value));
memo[memo.length] = value;
return memo;
}, []);
};
// Return a version of the array that does not contain the specified value(s).
_.without = function(array) {
return _.difference(array, slice.call(arguments, 1));
};
// Produce a duplicate-free version of the array. If the array has already
// been sorted, you have the option of using a faster algorithm.
// Aliased as `unique`.
_.uniq = _.unique = function(array, isSorted) {
return _.reduce(array, function(memo, el, i) {
if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) memo[memo.length] = el;
return memo;
}, []);
};
// Produce an array that contains the union: each distinct element from all of
// the passed-in arrays.
_.union = function() {
return _.uniq(_.flatten(arguments));
};
// Produce an array that contains every item shared between all the
// passed-in arrays. (Aliased as "intersect" for back-compat.)
_.intersection = _.intersect = function(array) {
var rest = slice.call(arguments, 1);
return _.filter(_.uniq(array), function(item) {
return _.every(rest, function(other) {
return _.indexOf(other, item) >= 0;
});
});
};
// Take the difference between one array and another.
// Only the elements present in just the first array will remain.
_.difference = function(array, other) {
return _.filter(array, function(value){ return !_.include(other, value); });
};
// Zip together multiple lists into a single array -- elements that share
// an index go together.
_.zip = function() {
var args = slice.call(arguments);
var length = _.max(_.pluck(args, 'length'));
var results = new Array(length);
for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i);
return results;
};
// If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
// we need this function. Return the position of the first occurrence of an
// item in an array, or -1 if the item is not included in the array.
// Delegates to **ECMAScript 5**'s native `indexOf` if available.
// If the array is large and already in sort order, pass `true`
// for **isSorted** to use binary search.
_.indexOf = function(array, item, isSorted) {
if (array == null) return -1;
var i, l;
if (isSorted) {
i = _.sortedIndex(array, item);
return array[i] === item ? i : -1;
}
if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
for (i = 0, l = array.length; i < l; i++) if (array[i] === item) return i;
return -1;
};
// Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
_.lastIndexOf = function(array, item) {
if (array == null) return -1;
if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item);
var i = array.length;
while (i--) if (array[i] === item) return i;
return -1;
};
// Generate an integer Array containing an arithmetic progression. A port of
// the native Python `range()` function. See
// [the Python documentation](http://docs.python.org/library/functions.html#range).
_.range = function(start, stop, step) {
if (arguments.length <= 1) {
stop = start || 0;
start = 0;
}
step = arguments[2] || 1;
var len = Math.max(Math.ceil((stop - start) / step), 0);
var idx = 0;
var range = new Array(len);
while(idx < len) {
range[idx++] = start;
start += step;
}
return range;
};
// Function (ahem) Functions
// ------------------
// Create a function bound to a given object (assigning `this`, and arguments,
// optionally). Binding with arguments is also known as `curry`.
// Delegates to **ECMAScript 5**'s native `Function.bind` if available.
// We check for `func.bind` first, to fail fast when `func` is undefined.
_.bind = function(func, obj) {
if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
var args = slice.call(arguments, 2);
return function() {
return func.apply(obj, args.concat(slice.call(arguments)));
};
};
// Bind all of an object's methods to that object. Useful for ensuring that
// all callbacks defined on an object belong to it.
_.bindAll = function(obj) {
var funcs = slice.call(arguments, 1);
if (funcs.length == 0) funcs = _.functions(obj);
each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
return obj;
};
// Memoize an expensive function by storing its results.
_.memoize = function(func, hasher) {
var memo = {};
hasher || (hasher = _.identity);
return function() {
var key = hasher.apply(this, arguments);
return hasOwnProperty.call(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
};
};
// Delays a function for the given number of milliseconds, and then calls
// it with the arguments supplied.
_.delay = function(func, wait) {
var args = slice.call(arguments, 2);
return setTimeout(function(){ return func.apply(func, args); }, wait);
};
// Defers a function, scheduling it to run after the current call stack has
// cleared.
_.defer = function(func) {
return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
};
// Internal function used to implement `_.throttle` and `_.debounce`.
var limit = function(func, wait, debounce) {
var timeout;
return function() {
var context = this, args = arguments;
var throttler = function() {
timeout = null;
func.apply(context, args);
};
if (debounce) clearTimeout(timeout);
if (debounce || !timeout) timeout = setTimeout(throttler, wait);
};
};
// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time.
_.throttle = function(func, wait) {
return limit(func, wait, false);
};
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds.
_.debounce = function(func, wait) {
return limit(func, wait, true);
};
// Returns a function that will be executed at most one time, no matter how
// often you call it. Useful for lazy initialization.
_.once = function(func) {
var ran = false, memo;
return function() {
if (ran) return memo;
ran = true;
return memo = func.apply(this, arguments);
};
};
// Returns the first function passed as an argument to the second,
// allowing you to adjust arguments, run code before and after, and
// conditionally execute the original function.
_.wrap = function(func, wrapper) {
return function() {
var args = [func].concat(slice.call(arguments));
return wrapper.apply(this, args);
};
};
// Returns a function that is the composition of a list of functions, each
// consuming the return value of the function that follows.
_.compose = function() {
var funcs = slice.call(arguments);
return function() {
var args = slice.call(arguments);
for (var i = funcs.length - 1; i >= 0; i--) {
args = [funcs[i].apply(this, args)];
}
return args[0];
};
};
// Returns a function that will only be executed after being called N times.
_.after = function(times, func) {
return function() {
if (--times < 1) { return func.apply(this, arguments); }
};
};
// Object Functions
// ----------------
// Retrieve the names of an object's properties.
// Delegates to **ECMAScript 5**'s native `Object.keys`
_.keys = nativeKeys || function(obj) {
if (obj !== Object(obj)) throw new TypeError('Invalid object');
var keys = [];
for (var key in obj) if (hasOwnProperty.call(obj, key)) keys[keys.length] = key;
return keys;
};
// Retrieve the values of an object's properties.
_.values = function(obj) {
return _.map(obj, _.identity);
};
// Return a sorted list of the function names available on the object.
// Aliased as `methods`
_.functions = _.methods = function(obj) {
var names = [];
for (var key in obj) {
if (_.isFunction(obj[key])) names.push(key);
}
return names.sort();
};
// Extend a given object with all the properties in passed-in object(s).
_.extend = function(obj) {
each(slice.call(arguments, 1), function(source) {
for (var prop in source) {
if (source[prop] !== void 0) obj[prop] = source[prop];
}
});
return obj;
};
// Fill in a given object with default properties.
_.defaults = function(obj) {
each(slice.call(arguments, 1), function(source) {
for (var prop in source) {
if (obj[prop] == null) obj[prop] = source[prop];
}
});
return obj;
};
// Create a (shallow-cloned) duplicate of an object.
_.clone = function(obj) {
return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
};
// Invokes interceptor with the obj, and then returns obj.
// The primary purpose of this method is to "tap into" a method chain, in
// order to perform operations on intermediate results within the chain.
_.tap = function(obj, interceptor) {
interceptor(obj);
return obj;
};
// Perform a deep comparison to check if two objects are equal.
_.isEqual = function(a, b) {
// Check object identity.
if (a === b) return true;
// Different types?
var atype = typeof(a), btype = typeof(b);
if (atype != btype) return false;
// Basic equality test (watch out for coercions).
if (a == b) return true;
// One is falsy and the other truthy.
if ((!a && b) || (a && !b)) return false;
// Unwrap any wrapped objects.
if (a._chain) a = a._wrapped;
if (b._chain) b = b._wrapped;
// One of them implements an isEqual()?
if (a.isEqual) return a.isEqual(b);
if (b.isEqual) return b.isEqual(a);
// Check dates' integer values.
if (_.isDate(a) && _.isDate(b)) return a.getTime() === b.getTime();
// Both are NaN?
if (_.isNaN(a) && _.isNaN(b)) return false;
// Compare regular expressions.
if (_.isRegExp(a) && _.isRegExp(b))
return a.source === b.source &&
a.global === b.global &&
a.ignoreCase === b.ignoreCase &&
a.multiline === b.multiline;
// If a is not an object by this point, we can't handle it.
if (atype !== 'object') return false;
// Check for different array lengths before comparing contents.
if (a.length && (a.length !== b.length)) return false;
// Nothing else worked, deep compare the contents.
var aKeys = _.keys(a), bKeys = _.keys(b);
// Different object sizes?
if (aKeys.length != bKeys.length) return false;
// Recursive comparison of contents.
for (var key in a) if (!(key in b) || !_.isEqual(a[key], b[key])) return false;
return true;
};
// Is a given array or object empty?
_.isEmpty = function(obj) {
if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
for (var key in obj) if (hasOwnProperty.call(obj, key)) return false;
return true;
};
// Is a given value a DOM element?
_.isElement = function(obj) {
return !!(obj && obj.nodeType == 1);
};
// Is a given value an array?
// Delegates to ECMA5's native Array.isArray
_.isArray = nativeIsArray || function(obj) {
return toString.call(obj) === '[object Array]';
};
// Is a given variable an object?
_.isObject = function(obj) {
return obj === Object(obj);
};
// Is a given variable an arguments object?
_.isArguments = function(obj) {
return !!(obj && hasOwnProperty.call(obj, 'callee'));
};
// Is a given value a function?
_.isFunction = function(obj) {
return !!(obj && obj.constructor && obj.call && obj.apply);
};
// Is a given value a string?
_.isString = function(obj) {
return !!(obj === '' || (obj && obj.charCodeAt && obj.substr));
};
// Is a given value a number?
_.isNumber = function(obj) {
return !!(obj === 0 || (obj && obj.toExponential && obj.toFixed));
};
// Is the given value `NaN`? `NaN` happens to be the only value in JavaScript
// that does not equal itself.
_.isNaN = function(obj) {
return obj !== obj;
};
// Is a given value a boolean?
_.isBoolean = function(obj) {
return obj === true || obj === false;
};
// Is a given value a date?
_.isDate = function(obj) {
return !!(obj && obj.getTimezoneOffset && obj.setUTCFullYear);
};
// Is the given value a regular expression?
_.isRegExp = function(obj) {
return !!(obj && obj.test && obj.exec && (obj.ignoreCase || obj.ignoreCase === false));
};
// Is a given value equal to null?
_.isNull = function(obj) {
return obj === null;
};
// Is a given variable undefined?
_.isUndefined = function(obj) {
return obj === void 0;
};
// Utility Functions
// -----------------
// Run Underscore.js in *noConflict* mode, returning the `_` variable to its
// previous owner. Returns a reference to the Underscore object.
_.noConflict = function() {
root._ = previousUnderscore;
return this;
};
// Keep the identity function around for default iterators.
_.identity = function(value) {
return value;
};
// Run a function **n** times.
_.times = function (n, iterator, context) {
for (var i = 0; i < n; i++) iterator.call(context, i);
};
// Add your own custom functions to the Underscore object, ensuring that
// they're correctly added to the OOP wrapper as well.
_.mixin = function(obj) {
each(_.functions(obj), function(name){
addToWrapper(name, _[name] = obj[name]);
});
};
// Generate a unique integer id (unique within the entire client session).
// Useful for temporary DOM ids.
var idCounter = 0;
_.uniqueId = function(prefix) {
var id = idCounter++;
return prefix ? prefix + id : id;
};
// By default, Underscore uses ERB-style template delimiters, change the
// following template settings to use alternative delimiters.
_.templateSettings = {
evaluate : /<%([\s\S]+?)%>/g,
interpolate : /<%=([\s\S]+?)%>/g
};
// JavaScript micro-templating, similar to John Resig's implementation.
// Underscore templating handles arbitrary delimiters, preserves whitespace,
// and correctly escapes quotes within interpolated code.
_.template = function(str, data) {
var c = _.templateSettings;
var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' +
'with(obj||{}){__p.push(\'' +
str.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(c.interpolate, function(match, code) {
return "'," + code.replace(/\\'/g, "'") + ",'";
})
.replace(c.evaluate || null, function(match, code) {
return "');" + code.replace(/\\'/g, "'")
.replace(/[\r\n\t]/g, ' ') + "__p.push('";
})
.replace(/\r/g, '\\r')
.replace(/\n/g, '\\n')
.replace(/\t/g, '\\t')
+ "');}return __p.join('');";
var func = new Function('obj', tmpl);
return data ? func(data) : func;
};
// The OOP Wrapper
// ---------------
// If Underscore is called as a function, it returns a wrapped object that
// can be used OO-style. This wrapper holds altered versions of all the
// underscore functions. Wrapped objects may be chained.
var wrapper = function(obj) { this._wrapped = obj; };
// Expose `wrapper.prototype` as `_.prototype`
_.prototype = wrapper.prototype;
// Helper function to continue chaining intermediate results.
var result = function(obj, chain) {
return chain ? _(obj).chain() : obj;
};
// A method to easily add functions to the OOP wrapper.
var addToWrapper = function(name, func) {
wrapper.prototype[name] = function() {
var args = slice.call(arguments);
unshift.call(args, this._wrapped);
return result(func.apply(_, args), this._chain);
};
};
// Add all of the Underscore functions to the wrapper object.
_.mixin(_);
// Add all mutator Array functions to the wrapper.
each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
var method = ArrayProto[name];
wrapper.prototype[name] = function() {
method.apply(this._wrapped, arguments);
return result(this._wrapped, this._chain);
};
});
// Add all accessor Array functions to the wrapper.
each(['concat', 'join', 'slice'], function(name) {
var method = ArrayProto[name];
wrapper.prototype[name] = function() {
return result(method.apply(this._wrapped, arguments), this._chain);
};
});
// Start chaining a wrapped Underscore object.
wrapper.prototype.chain = function() {
this._chain = true;
return this;
};
// Extracts the result from a wrapped and chained object.
wrapper.prototype.value = function() {
return this._wrapped;
};
})();
/*!
* bean.js - copyright Jacob Thornton 2011
* https://github.com/fat/bean
* MIT License
* special thanks to:
* dean edwards: http://dean.edwards.name/
* dperini: https://github.com/dperini/nwevents
* the entire mootools team: github.com/mootools/mootools-core
*/
/*global module:true, define:true*/
!function (name, context, definition) {
if (typeof module !== 'undefined') module.exports = definition(name, context);
else if (typeof define === 'function' && typeof define.amd === 'object') define(definition);
else context[name] = definition(name, context);
}('bean', this, function (name, context) {
var win = window
, old = context[name]
, overOut = /over|out/
, namespaceRegex = /[^\.]*(?=\..*)\.|.*/
, nameRegex = /\..*/
, addEvent = 'addEventListener'
, attachEvent = 'attachEvent'
, removeEvent = 'removeEventListener'
, detachEvent = 'detachEvent'
, doc = document || {}
, root = doc.documentElement || {}
, W3C_MODEL = root[addEvent]
, eventSupport = W3C_MODEL ? addEvent : attachEvent
, slice = Array.prototype.slice
, mouseTypeRegex = /click|mouse|menu|drag|drop/i
, touchTypeRegex = /^touch|^gesture/i
, ONE = { one: 1 } // singleton for quick matching making add() do one()
, nativeEvents = (function (hash, events, i) {
for (i = 0; i < events.length; i++)
hash[events[i]] = 1
return hash
})({}, (
'click dblclick mouseup mousedown contextmenu ' + // mouse buttons
'mousewheel DOMMouseScroll ' + // mouse wheel
'mouseover mouseout mousemove selectstart selectend ' + // mouse movement
'keydown keypress keyup ' + // keyboard
'orientationchange ' + // mobile
'focus blur change reset select submit ' + // form elements
'load unload beforeunload resize move DOMContentLoaded readystatechange ' + // window
'error abort scroll ' + // misc
(W3C_MODEL ? // element.fireEvent('onXYZ'... is not forgiving if we try to fire an event
// that doesn't actually exist, so make sure we only do these on newer browsers
'show ' + // mouse buttons
'input invalid ' + // form elements
'touchstart touchmove touchend touchcancel ' + // touch
'gesturestart gesturechange gestureend ' + // gesture
'message readystatechange pageshow pagehide popstate ' + // window
'hashchange offline online ' + // window
'afterprint beforeprint ' + // printing
'dragstart dragenter dragover dragleave drag drop dragend ' + // dnd
'loadstart progress suspend emptied stalled loadmetadata ' + // media
'loadeddata canplay canplaythrough playing waiting seeking ' + // media
'seeked ended durationchange timeupdate play pause ratechange ' + // media
'volumechange cuechange ' + // media
'checking noupdate downloading cached updateready obsolete ' + // appcache
'' : '')
).split(' ')
)
, customEvents = (function () {
function isDescendant(parent, node) {
while ((node = node.parentNode) !== null) {
if (node === parent) return true
}
return false
}
function check(event) {
var related = event.relatedTarget
if (!related) return related === null
return (related !== this && related.prefix !== 'xul' && !/document/.test(this.toString()) && !isDescendant(this, related))
}
return {
mouseenter: { base: 'mouseover', condition: check }
, mouseleave: { base: 'mouseout', condition: check }
, mousewheel: { base: /Firefox/.test(navigator.userAgent) ? 'DOMMouseScroll' : 'mousewheel' }
}
})()
, fixEvent = (function () {
var commonProps = 'altKey attrChange attrName bubbles cancelable ctrlKey currentTarget detail eventPhase getModifierState isTrusted metaKey relatedNode relatedTarget shiftKey srcElement target timeStamp type view which'.split(' ')
, mouseProps = commonProps.concat('button buttons clientX clientY dataTransfer fromElement offsetX offsetY pageX pageY screenX screenY toElement'.split(' '))
, keyProps = commonProps.concat('char charCode key keyCode'.split(' '))
, touchProps = commonProps.concat('touches targetTouches changedTouches scale rotation'.split(' '))
, preventDefault = 'preventDefault'
, createPreventDefault = function (event) {
return function () {
if (event[preventDefault])
event[preventDefault]()
else
event.returnValue = false
}
}
, stopPropagation = 'stopPropagation'
, createStopPropagation = function (event) {
return function () {
if (event[stopPropagation])
event[stopPropagation]()
else
event.cancelBubble = true
}
}
, createStop = function (synEvent) {
return function () {
synEvent[preventDefault]()
synEvent[stopPropagation]()
synEvent.stopped = true
}
}
, copyProps = function (event, result, props) {
var i, p
for (i = props.length; i--;) {
p = props[i]
if (!(p in result) && p in event) result[p] = event[p]
}
}
return function (event, isNative) {
var result = { originalEvent: event, isNative: isNative }
if (!event)
return result
var props
, type = event.type
, target = event.target || event.srcElement
result[preventDefault] = createPreventDefault(event)
result[stopPropagation] = createStopPropagation(event)
result.stop = createStop(result)
result.target = target && target.nodeType === 3 ? target.parentNode : target
if (isNative) { // we only need basic augmentation on custom events, the rest is too expensive
if (type.indexOf('key') !== -1) {
props = keyProps
result.keyCode = event.which || event.keyCode
} else if (mouseTypeRegex.test(type)) {
props = mouseProps
result.rightClick = event.which === 3 || event.button === 2
result.pos = { x: 0, y: 0 }
if (event.pageX || event.pageY) {
result.clientX = event.pageX
result.clientY = event.pageY
} else if (event.clientX || event.clientY) {
result.clientX = event.clientX + doc.body.scrollLeft + root.scrollLeft
result.clientY = event.clientY + doc.body.scrollTop + root.scrollTop
}
if (overOut.test(type))
result.relatedTarget = event.relatedTarget || event[(type === 'mouseover' ? 'from' : 'to') + 'Element']
} else if (touchTypeRegex.test(type)) {
props = touchProps
}
copyProps(event, result, props || commonProps)
}
return result
}
})()
// if we're in old IE we can't do onpropertychange on doc or win so we use doc.documentElement for both
, targetElement = function (element, isNative) {
return !W3C_MODEL && !isNative && (element === doc || element === win) ? root : element
}
// we use one of these per listener, of any type
, RegEntry = (function () {
function entry(element, type, handler, original, namespaces) {
this.element = element
this.type = type
this.handler = handler
this.original = original
this.namespaces = namespaces
this.custom = customEvents[type]
this.isNative = nativeEvents[type] && element[eventSupport]
this.eventType = W3C_MODEL || this.isNative ? type : 'propertychange'
this.customType = !W3C_MODEL && !this.isNative && type
this.target = targetElement(element, this.isNative)
this.eventSupport = this.target[eventSupport]
}
entry.prototype = {
// given a list of namespaces, is our entry in any of them?
inNamespaces: function (checkNamespaces) {
var i, j
if (!checkNamespaces)
return true
if (!this.namespaces)
return false
for (i = checkNamespaces.length; i--;) {
for (j = this.namespaces.length; j--;) {
if (checkNamespaces[i] === this.namespaces[j])
return true
}
}
return false
}
// match by element, original fn (opt), handler fn (opt)
, matches: function (checkElement, checkOriginal, checkHandler) {
return this.element === checkElement &&
(!checkOriginal || this.original === checkOriginal) &&
(!checkHandler || this.handler === checkHandler)
}
}
return entry
})()
, registry = (function () {
// our map stores arrays by event type, just because it's better than storing
// everything in a single array. uses '$' as a prefix for the keys for safety
var map = {}
// generic functional search of our registry for matching listeners,
// `fn` returns false to break out of the loop
, forAll = function (element, type, original, handler, fn) {
if (!type || type === '*') {
// search the whole registry
for (var t in map) {
if (t.charAt(0) === '$')
forAll(element, t.substr(1), original, handler, fn)
}
} else {
var i = 0, l, list = map['$' + type], all = element === '*'
if (!list)
return
for (l = list.length; i < l; i++) {
if (all || list[i].matches(element, original, handler))
if (!fn(list[i], list, i, type))
return
}
}
}
, has = function (element, type, original) {
// we're not using forAll here simply because it's a bit slower and this
// needs to be fast
var i, list = map['$' + type]
if (list) {
for (i = list.length; i--;) {
if (list[i].matches(element, original, null))
return true
}
}
return false
}
, get = function (element, type, original) {
var entries = []
forAll(element, type, original, null, function (entry) { return entries.push(entry) })
return entries
}
, put = function (entry) {
(map['$' + entry.type] || (map['$' + entry.type] = [])).push(entry)
return entry
}
, del = function (entry) {
forAll(entry.element, entry.type, null, entry.handler, function (entry, list, i) {
list.splice(i, 1)
if (list.length === 0)
delete map['$' + entry.type]
return false
})
}
// dump all entries, used for onunload
, entries = function () {
var t, entries = []
for (t in map) {
if (t.charAt(0) === '$')
entries = entries.concat(map[t])
}
return entries
}
return { has: has, get: get, put: put, del: del, entries: entries }
})()
// add and remove listeners to DOM elements
, listener = W3C_MODEL ? function (element, type, fn, add) {
element[add ? addEvent : removeEvent](type, fn, false)
} : function (element, type, fn, add, custom) {
if (custom && add && element['_on' + custom] === null)
element['_on' + custom] = 0
element[add ? attachEvent : detachEvent]('on' + type, fn)
}
, nativeHandler = function (element, fn, args) {
return function (event) {
event = fixEvent(event || ((this.ownerDocument || this.document || this).parentWindow || win).event, true)
return fn.apply(element, [event].concat(args))
}
}
, customHandler = function (element, fn, type, condition, args, isNative) {
return function (event) {
if (condition ? condition.apply(this, arguments) : W3C_MODEL ? true : event && event.propertyName === '_on' + type || !event) {
if (event)
event = fixEvent(event || ((this.ownerDocument || this.document || this).parentWindow || win).event, isNative)
fn.apply(element, event && (!args || args.length === 0) ? arguments : slice.call(arguments, event ? 0 : 1).concat(args))
}
}
}
, once = function (rm, element, type, fn, originalFn) {
// wrap the handler in a handler that does a remove as well
return function () {
rm(element, type, originalFn)
fn.apply(this, arguments)
}
}
, removeListener = function (element, orgType, handler, namespaces) {
var i, l, entry
, type = (orgType && orgType.replace(nameRegex, ''))
, handlers = registry.get(element, type, handler)
for (i = 0, l = handlers.length; i < l; i++) {
if (handlers[i].inNamespaces(namespaces)) {
if ((entry = handlers[i]).eventSupport)
listener(entry.target, entry.eventType, entry.handler, false, entry.type)
// TODO: this is problematic, we have a registry.get() and registry.del() that
// both do registry searches so we waste cycles doing this. Needs to be rolled into
// a single registry.forAll(fn) that removes while finding, but the catch is that
// we'll be splicing the arrays that we're iterating over. Needs extra tests to
// make sure we don't screw it up. @rvagg
registry.del(entry)
}
}
}
, addListener = function (element, orgType, fn, originalFn, args) {
var entry
, type = orgType.replace(nameRegex, '')
, namespaces = orgType.replace(namespaceRegex, '').split('.')
if (registry.has(element, type, fn))
return element // no dupe
if (type === 'unload')
fn = once(removeListener, element, type, fn, originalFn) // self clean-up
if (customEvents[type]) {
if (customEvents[type].condition)
fn = customHandler(element, fn, type, customEvents[type].condition, true)
type = customEvents[type].base || type
}
entry = registry.put(new RegEntry(element, type, fn, originalFn, namespaces[0] && namespaces))
entry.handler = entry.isNative ?
nativeHandler(element, entry.handler, args) :
customHandler(element, entry.handler, type, false, args, false)
if (entry.eventSupport)
listener(entry.target, entry.eventType, entry.handler, true, entry.customType)
}
, del = function (selector, fn, $) {
return function (e) {
var target, i, array = typeof selector === 'string' ? $(selector, this) : selector
for (target = e.target; target && target !== this; target = target.parentNode) {
for (i = array.length; i--;) {
if (array[i] === target) {
return fn.apply(target, arguments)
}
}
}
}
}
, remove = function (element, typeSpec, fn) {
var k, m, type, namespaces, i
, rm = removeListener
, isString = typeSpec && typeof typeSpec === 'string'
if (isString && typeSpec.indexOf(' ') > 0) {
// remove(el, 't1 t2 t3', fn) or remove(el, 't1 t2 t3')
typeSpec = typeSpec.split(' ')
for (i = typeSpec.length; i--;)
remove(element, typeSpec[i], fn)
return element
}
type = isString && typeSpec.replace(nameRegex, '')
if (type && customEvents[type])
type = customEvents[type].type
if (!typeSpec || isString) {
// remove(el) or remove(el, t1.ns) or remove(el, .ns) or remove(el, .ns1.ns2.ns3)
if (namespaces = isString && typeSpec.replace(namespaceRegex, ''))
namespaces = namespaces.split('.')
rm(element, type, fn, namespaces)
} else if (typeof typeSpec === 'function') {
// remove(el, fn)
rm(element, null, typeSpec)
} else {
// remove(el, { t1: fn1, t2, fn2 })
for (k in typeSpec) {
if (typeSpec.hasOwnProperty(k))
remove(element, k, typeSpec[k])
}
}
return element
}
, add = function (element, events, fn, delfn, $) {
var type, types, i, args
, originalFn = fn
, isDel = fn && typeof fn === 'string'
if (events && !fn && typeof events === 'object') {
for (type in events) {
if (events.hasOwnProperty(type))
add.apply(this, [ element, type, events[type] ])
}
} else {
args = arguments.length > 3 ? slice.call(arguments, 3) : []
types = (isDel ? fn : events).split(' ')
isDel && (fn = del(events, (originalFn = delfn), $)) && (args = slice.call(args, 1))
// special case for one()
this === ONE && (fn = once(remove, element, events, fn, originalFn))
for (i = types.length; i--;) addListener(element, types[i], fn, originalFn, args)
}
return element
}
, one = function () {
return add.apply(ONE, arguments)
}
, fireListener = W3C_MODEL ? function (isNative, type, element) {
var evt = doc.createEvent(isNative ? 'HTMLEvents' : 'UIEvents')
evt[isNative ? 'initEvent' : 'initUIEvent'](type, true, true, win, 1)
element.dispatchEvent(evt)
} : function (isNative, type, element) {
element = targetElement(element, isNative)
// if not-native then we're using onpropertychange so we just increment a custom property
isNative ? element.fireEvent('on' + type, doc.createEventObject()) : element['_on' + type]++
}
, fire = function (element, type, args) {
var i, j, l, names, handlers
, types = type.split(' ')
for (i = types.length; i--;) {
type = types[i].replace(nameRegex, '')
if (names = types[i].replace(namespaceRegex, ''))
names = names.split('.')
if (!names && !args && element[eventSupport]) {
fireListener(nativeEvents[type], type, element)
} else {
// non-native event, either because of a namespace, arguments or a non DOM element
// iterate over all listeners and manually 'fire'
handlers = registry.get(element, type)
args = [false].concat(args)
for (j = 0, l = handlers.length; j < l; j++) {
if (handlers[j].inNamespaces(names))
handlers[j].handler.apply(element, args)
}
}
}
return element
}
, clone = function (element, from, type) {
var i = 0
, handlers = registry.get(from, type)
, l = handlers.length
for (;i < l; i++)
handlers[i].original && add(element, handlers[i].type, handlers[i].original)
return element
}
, bean = {
add: add
, one: one
, remove: remove
, clone: clone
, fire: fire
, noConflict: function () {
context[name] = old
return this
}
}
if (win[attachEvent]) {
// for IE, clean up on unload to avoid leaks
var cleanup = function () {
var i, entries = registry.entries()
for (i in entries) {
if (entries[i].type && entries[i].type !== 'unload')
remove(entries[i].element, entries[i].type)
}
win[detachEvent]('onunload', cleanup)
win.CollectGarbage && win.CollectGarbage()
}
win[attachEvent]('onunload', cleanup)
}
return bean
});
/**
* Flotr2 (c) 2012 Carl Sutherland
* MIT License
* Special thanks to:
* Flotr: http://code.google.com/p/flotr/ (fork)
* Flot: https://github.com/flot/flot (original fork)
*/
(function () {
var
global = this,
previousFlotr = this.Flotr,
Flotr;
Flotr = {
_: _,
bean: bean,
isIphone: /iphone/i.test(navigator.userAgent),
isIE: (navigator.appVersion.indexOf("MSIE") != -1 ? parseFloat(navigator.appVersion.split("MSIE")[1]) : false),
/**
* An object of the registered graph types. Use Flotr.addType(type, object)
* to add your own type.
*/
graphTypes: {},
/**
* The list of the registered plugins
*/
plugins: {},
/**
* Can be used to add your own chart type.
* @param {String} name - Type of chart, like 'pies', 'bars' etc.
* @param {String} graphType - The object containing the basic drawing functions (draw, etc)
*/
addType: function(name, graphType){
Flotr.graphTypes[name] = graphType;
Flotr.defaultOptions[name] = graphType.options || {};
Flotr.defaultOptions.defaultType = Flotr.defaultOptions.defaultType || name;
},
/**
* Can be used to add a plugin
* @param {String} name - The name of the plugin
* @param {String} plugin - The object containing the plugin's data (callbacks, options, function1, function2, ...)
*/
addPlugin: function(name, plugin){
Flotr.plugins[name] = plugin;
Flotr.defaultOptions[name] = plugin.options || {};
},
/**
* Draws the graph. This function is here for backwards compatibility with Flotr version 0.1.0alpha.
* You could also draw graphs by directly calling Flotr.Graph(element, data, options).
* @param {Element} el - element to insert the graph into
* @param {Object} data - an array or object of dataseries
* @param {Object} options - an object containing options
* @param {Class} _GraphKlass_ - (optional) Class to pass the arguments to, defaults to Flotr.Graph
* @return {Object} returns a new graph object and of course draws the graph.
*/
draw: function(el, data, options, GraphKlass){
GraphKlass = GraphKlass || Flotr.Graph;
return new GraphKlass(el, data, options);
},
/**
* Recursively merges two objects.
* @param {Object} src - source object (likely the object with the least properties)
* @param {Object} dest - destination object (optional, object with the most properties)
* @return {Object} recursively merged Object
* @TODO See if we can't remove this.
*/
merge: function(src, dest){
var i, v, result = dest || {};
for (i in src) {
v = src[i];
if (v && typeof(v) === 'object') {
if (v.constructor === Array) {
result[i] = this._.clone(v);
} else if (v.constructor !== RegExp && !this._.isElement(v)) {
result[i] = Flotr.merge(v, (dest ? dest[i] : undefined));
} else {
result[i] = v;
}
} else {
result[i] = v;
}
}
return result;
},
/**
* Recursively clones an object.
* @param {Object} object - The object to clone
* @return {Object} the clone
* @TODO See if we can't remove this.
*/
clone: function(object){
return Flotr.merge(object, {});
},
/**
* Function calculates the ticksize and returns it.
* @param {Integer} noTicks - number of ticks
* @param {Integer} min - lower bound integer value for the current axis
* @param {Integer} max - upper bound integer value for the current axis
* @param {Integer} decimals - number of decimals for the ticks
* @return {Integer} returns the ticksize in pixels
*/
getTickSize: function(noTicks, min, max, decimals){
var delta = (max - min) / noTicks,
magn = Flotr.getMagnitude(delta),
tickSize = 10,
norm = delta / magn; // Norm is between 1.0 and 10.0.
if(norm < 1.5) tickSize = 1;
else if(norm < 2.25) tickSize = 2;
else if(norm < 3) tickSize = ((decimals === 0) ? 2 : 2.5);
else if(norm < 7.5) tickSize = 5;
return tickSize * magn;
},
/**
* Default tick formatter.
* @param {String, Integer} val - tick value integer
* @param {Object} axisOpts - the axis' options
* @return {String} formatted tick string
*/
defaultTickFormatter: function(val, axisOpts){
return val+'';
},
/**
* Formats the mouse tracker values.
* @param {Object} obj - Track value Object {x:..,y:..}
* @return {String} Formatted track string
*/
defaultTrackFormatter: function(obj){
return '('+obj.x+', '+obj.y+')';
},
/**
* Utility function to convert file size values in bytes to kB, MB, ...
* @param value {Number} - The value to convert
* @param precision {Number} - The number of digits after the comma (default: 2)
* @param base {Number} - The base (default: 1000)
*/
engineeringNotation: function(value, precision, base){
var sizes = ['Y','Z','E','P','T','G','M','k',''],
fractionSizes = ['y','z','a','f','p','n','µ','m',''],
total = sizes.length;
base = base || 1000;
precision = Math.pow(10, precision || 2);
if (value === 0) return 0;
if (value > 1) {
while (total-- && (value >= base)) value /= base;
}
else {
sizes = fractionSizes;
total = sizes.length;
while (total-- && (value < 1)) value *= base;
}
return (Math.round(value * precision) / precision) + sizes[total];
},
/**
* Returns the magnitude of the input value.
* @param {Integer, Float} x - integer or float value
* @return {Integer, Float} returns the magnitude of the input value
*/
getMagnitude: function(x){
return Math.pow(10, Math.floor(Math.log(x) / Math.LN10));
},
toPixel: function(val){
return Math.floor(val)+0.5;//((val-Math.round(val) < 0.4) ? (Math.floor(val)-0.5) : val);
},
toRad: function(angle){
return -angle * (Math.PI/180);
},
floorInBase: function(n, base) {
return base * Math.floor(n / base);
},
drawText: function(ctx, text, x, y, style) {
if (!ctx.fillText) {
ctx.drawText(text, x, y, style);
return;
}
style = this._.extend({
size: Flotr.defaultOptions.fontSize,
color: '#000000',
textAlign: 'left',
textBaseline: 'bottom',
weight: 1,
angle: 0
}, style);
ctx.save();
ctx.translate(x, y);
ctx.rotate(style.angle);
ctx.fillStyle = style.color;
ctx.font = (style.weight > 1 ? "bold " : "") + (style.size*1.3) + "px sans-serif";
ctx.textAlign = style.textAlign;
ctx.textBaseline = style.textBaseline;
ctx.fillText(text, 0, 0);
ctx.restore();
},
getBestTextAlign: function(angle, style) {
style = style || {textAlign: 'center', textBaseline: 'middle'};
angle += Flotr.getTextAngleFromAlign(style);
if (Math.abs(Math.cos(angle)) > 10e-3)
style.textAlign = (Math.cos(angle) > 0 ? 'right' : 'left');
if (Math.abs(Math.sin(angle)) > 10e-3)
style.textBaseline = (Math.sin(angle) > 0 ? 'top' : 'bottom');
return style;
},
alignTable: {
'right middle' : 0,
'right top' : Math.PI/4,
'center top' : Math.PI/2,
'left top' : 3*(Math.PI/4),
'left middle' : Math.PI,
'left bottom' : -3*(Math.PI/4),
'center bottom': -Math.PI/2,
'right bottom' : -Math.PI/4,
'center middle': 0
},
getTextAngleFromAlign: function(style) {
return Flotr.alignTable[style.textAlign+' '+style.textBaseline] || 0;
},
noConflict : function () {
global.Flotr = previousFlotr;
return this;
}
};
global.Flotr = Flotr;
})();
/**
* Flotr Defaults
*/
Flotr.defaultOptions = {
colors: ['#00A8F0', '#C0D800', '#CB4B4B', '#4DA74D', '#9440ED'], //=> The default colorscheme. When there are > 5 series, additional colors are generated.
ieBackgroundColor: '#FFFFFF', // Background color for excanvas clipping
title: null, // => The graph's title
subtitle: null, // => The graph's subtitle
shadowSize: 4, // => size of the 'fake' shadow
defaultType: null, // => default series type
HtmlText: true, // => wether to draw the text using HTML or on the canvas
fontColor: '#545454', // => default font color
fontSize: 7.5, // => canvas' text font size
resolution: 1, // => resolution of the graph, to have printer-friendly graphs !
parseFloat: true, // => whether to preprocess data for floats (ie. if input is string)
preventDefault: true, // => preventDefault by default for mobile events. Turn off to enable scroll.
xaxis: {
ticks: null, // => format: either [1, 3] or [[1, 'a'], 3]
minorTicks: null, // => format: either [1, 3] or [[1, 'a'], 3]
showLabels: true, // => setting to true will show the axis ticks labels, hide otherwise
showMinorLabels: false,// => true to show the axis minor ticks labels, false to hide
labelsAngle: 0, // => labels' angle, in degrees
title: null, // => axis title
titleAngle: 0, // => axis title's angle, in degrees
noTicks: 5, // => number of ticks for automagically generated ticks
minorTickFreq: null, // => number of minor ticks between major ticks for autogenerated ticks
tickFormatter: Flotr.defaultTickFormatter, // => fn: number, Object -> string
tickDecimals: null, // => no. of decimals, null means auto
min: null, // => min. value to show, null means set automatically
max: null, // => max. value to show, null means set automatically
autoscale: false, // => Turns autoscaling on with true
autoscaleMargin: 0, // => margin in % to add if auto-setting min/max
color: null, // => color of the ticks
mode: 'normal', // => can be 'time' or 'normal'
timeFormat: null,
timeMode:'UTC', // => For UTC time ('local' for local time).
timeUnit:'millisecond',// => Unit for time (millisecond, second, minute, hour, day, month, year)
scaling: 'linear', // => Scaling, can be 'linear' or 'logarithmic'
base: Math.E,
titleAlign: 'center',
margin: true // => Turn off margins with false
},
x2axis: {},
yaxis: {
ticks: null, // => format: either [1, 3] or [[1, 'a'], 3]
minorTicks: null, // => format: either [1, 3] or [[1, 'a'], 3]
showLabels: true, // => setting to true will show the axis ticks labels, hide otherwise
showMinorLabels: false,// => true to show the axis minor ticks labels, false to hide
labelsAngle: 0, // => labels' angle, in degrees
title: null, // => axis title
titleAngle: 90, // => axis title's angle, in degrees
noTicks: 5, // => number of ticks for automagically generated ticks
minorTickFreq: null, // => number of minor ticks between major ticks for autogenerated ticks
tickFormatter: Flotr.defaultTickFormatter, // => fn: number, Object -> string
tickDecimals: null, // => no. of decimals, null means auto
min: null, // => min. value to show, null means set automatically
max: null, // => max. value to show, null means set automatically
autoscale: false, // => Turns autoscaling on with true
autoscaleMargin: 0, // => margin in % to add if auto-setting min/max
color: null, // => The color of the ticks
scaling: 'linear', // => Scaling, can be 'linear' or 'logarithmic'
base: Math.E,
titleAlign: 'center',
margin: true // => Turn off margins with false
},
y2axis: {
titleAngle: 270
},
grid: {
color: '#545454', // => primary color used for outline and labels
backgroundColor: null, // => null for transparent, else color
backgroundImage: null, // => background image. String or object with src, left and top
watermarkAlpha: 0.4, // =>
tickColor: '#DDDDDD', // => color used for the ticks
labelMargin: 3, // => margin in pixels
verticalLines: true, // => whether to show gridlines in vertical direction
minorVerticalLines: null, // => whether to show gridlines for minor ticks in vertical dir.
horizontalLines: true, // => whether to show gridlines in horizontal direction
minorHorizontalLines: null, // => whether to show gridlines for minor ticks in horizontal dir.
outlineWidth: 1, // => width of the grid outline/border in pixels
outline : 'nsew', // => walls of the outline to display
circular: false // => if set to true, the grid will be circular, must be used when radars are drawn
},
mouse: {
track: false, // => true to track the mouse, no tracking otherwise
trackAll: false,
position: 'se', // => position of the value box (default south-east)
relative: false, // => next to the mouse cursor
trackFormatter: Flotr.defaultTrackFormatter, // => formats the values in the value box
margin: 5, // => margin in pixels of the valuebox
lineColor: '#FF3F19', // => line color of points that are drawn when mouse comes near a value of a series
trackDecimals: 1, // => decimals for the track values
sensibility: 2, // => the lower this number, the more precise you have to aim to show a value
trackY: true, // => whether or not to track the mouse in the y axis
radius: 3, // => radius of the track point
fillColor: null, // => color to fill our select bar with only applies to bar and similar graphs (only bars for now)
fillOpacity: 0.4 // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
}
};
/**
* Flotr Color
*/
(function () {
var
_ = Flotr._;
// Constructor
function Color (r, g, b, a) {
this.rgba = ['r','g','b','a'];
var x = 4;
while(-1<--x){
this[this.rgba[x]] = arguments[x] || ((x==3) ? 1.0 : 0);
}
this.normalize();
}
// Constants
var COLOR_NAMES = {
aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],
brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],
darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],
darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],
darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],
khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],
lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],
maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],
violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]
};
Color.prototype = {
scale: function(rf, gf, bf, af){
var x = 4;
while (-1 < --x) {
if (!_.isUndefined(arguments[x])) this[this.rgba[x]] *= arguments[x];
}
return this.normalize();
},
alpha: function(alpha) {
if (!_.isUndefined(alpha) && !_.isNull(alpha)) {
this.a = alpha;
}
return this.normalize();
},
clone: function(){
return new Color(this.r, this.b, this.g, this.a);
},
limit: function(val,minVal,maxVal){
return Math.max(Math.min(val, maxVal), minVal);
},
normalize: function(){
var limit = this.limit;
this.r = limit(parseInt(this.r, 10), 0, 255);
this.g = limit(parseInt(this.g, 10), 0, 255);
this.b = limit(parseInt(this.b, 10), 0, 255);
this.a = limit(this.a, 0, 1);
return this;
},
distance: function(color){
if (!color) return;
color = new Color.parse(color);
var dist = 0, x = 3;
while(-1<--x){
dist += Math.abs(this[this.rgba[x]] - color[this.rgba[x]]);
}
return dist;
},
toString: function(){
return (this.a >= 1.0) ? 'rgb('+[this.r,this.g,this.b].join(',')+')' : 'rgba('+[this.r,this.g,this.b,this.a].join(',')+')';
},
contrast: function () {
var
test = 1 - ( 0.299 * this.r + 0.587 * this.g + 0.114 * this.b) / 255;
return (test < 0.5 ? '#000000' : '#ffffff');
}
};
_.extend(Color, {
/**
* Parses a color string and returns a corresponding Color.
* The different tests are in order of probability to improve speed.
* @param {String, Color} str - string thats representing a color
* @return {Color} returns a Color object or false
*/
parse: function(color){
if (color instanceof Color) return color;
var result;
// #a0b1c2
if((result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color)))
return new Color(parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16));
// rgb(num,num,num)
if((result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color)))
return new Color(parseInt(result[1], 10), parseInt(result[2], 10), parseInt(result[3], 10));
// #fff
if((result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color)))
return new Color(parseInt(result[1]+result[1],16), parseInt(result[2]+result[2],16), parseInt(result[3]+result[3],16));
// rgba(num,num,num,num)
if((result = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(color)))
return new Color(parseInt(result[1], 10), parseInt(result[2], 10), parseInt(result[3], 10), parseFloat(result[4]));
// rgb(num%,num%,num%)
if((result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(color)))
return new Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55);
// rgba(num%,num%,num%,num)
if((result = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(color)))
return new Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55, parseFloat(result[4]));
// Otherwise, we're most likely dealing with a named color.
var name = (color+'').replace(/^\s*([\S\s]*?)\s*$/, '$1').toLowerCase();
if(name == 'transparent'){
return new Color(255, 255, 255, 0);
}
return (result = COLOR_NAMES[name]) ? new Color(result[0], result[1], result[2]) : new Color(0, 0, 0, 0);
},
/**
* Process color and options into color style.
*/
processColor: function(color, options) {
var opacity = options.opacity;
if (!color) return 'rgba(0, 0, 0, 0)';
if (color instanceof Color) return color.alpha(opacity).toString();
if (_.isString(color)) return Color.parse(color).alpha(opacity).toString();
var grad = color.colors ? color : {colors: color};
if (!options.ctx) {
if (!_.isArray(grad.colors)) return 'rgba(0, 0, 0, 0)';
return Color.parse(_.isArray(grad.colors[0]) ? grad.colors[0][1] : grad.colors[0]).alpha(opacity).toString();
}
grad = _.extend({start: 'top', end: 'bottom'}, grad);
if (/top/i.test(grad.start)) options.x1 = 0;
if (/left/i.test(grad.start)) options.y1 = 0;
if (/bottom/i.test(grad.end)) options.x2 = 0;
if (/right/i.test(grad.end)) options.y2 = 0;
var i, c, stop, gradient = options.ctx.createLinearGradient(options.x1, options.y1, options.x2, options.y2);
for (i = 0; i < grad.colors.length; i++) {
c = grad.colors[i];
if (_.isArray(c)) {
stop = c[0];
c = c[1];
}
else stop = i / (grad.colors.length-1);
gradient.addColorStop(stop, Color.parse(c).alpha(opacity));
}
return gradient;
}
});
Flotr.Color = Color;
})();
/**
* Flotr Date
*/
Flotr.Date = {
set : function (date, name, mode, value) {
mode = mode || 'UTC';
name = 'set' + (mode === 'UTC' ? 'UTC' : '') + name;
date[name](value);
},
get : function (date, name, mode) {
mode = mode || 'UTC';
name = 'get' + (mode === 'UTC' ? 'UTC' : '') + name;
return date[name]();
},
format: function(d, format, mode) {
if (!d) return;
// We should maybe use an "official" date format spec, like PHP date() or ColdFusion
// http://fr.php.net/manual/en/function.date.php
// http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=functions_c-d_29.html
var
get = this.get,
tokens = {
h: get(d, 'Hours', mode).toString(),
H: leftPad(get(d, 'Hours', mode)),
M: leftPad(get(d, 'Minutes', mode)),
S: leftPad(get(d, 'Seconds', mode)),
s: get(d, 'Milliseconds', mode),
d: get(d, 'Date', mode).toString(),
m: (get(d, 'Month', mode) + 1).toString(),
y: get(d, 'FullYear', mode).toString(),
b: Flotr.Date.monthNames[get(d, 'Month', mode)]
};
function leftPad(n){
n += '';
return n.length == 1 ? "0" + n : n;
}
var r = [], c,
escape = false;
for (var i = 0; i < format.length; ++i) {
c = format.charAt(i);
if (escape) {
r.push(tokens[c] || c);
escape = false;
}
else if (c == "%")
escape = true;
else
r.push(c);
}
return r.join('');
},
getFormat: function(time, span) {
var tu = Flotr.Date.timeUnits;
if (time < tu.second) return "%h:%M:%S.%s";
else if (time < tu.minute) return "%h:%M:%S";
else if (time < tu.day) return (span < 2 * tu.day) ? "%h:%M" : "%b %d %h:%M";
else if (time < tu.month) return "%b %d";
else if (time < tu.year) return (span < tu.year) ? "%b" : "%b %y";
else return "%y";
},
formatter: function (v, axis) {
var
options = axis.options,
scale = Flotr.Date.timeUnits[options.timeUnit],
d = new Date(v * scale);
// first check global format
if (axis.options.timeFormat)
return Flotr.Date.format(d, options.timeFormat, options.timeMode);
var span = (axis.max - axis.min) * scale,
t = axis.tickSize * Flotr.Date.timeUnits[axis.tickUnit];
return Flotr.Date.format(d, Flotr.Date.getFormat(t, span), options.timeMode);
},
generator: function(axis) {
var
set = this.set,
get = this.get,
timeUnits = this.timeUnits,
spec = this.spec,
options = axis.options,
mode = options.timeMode,
scale = timeUnits[options.timeUnit],
min = axis.min * scale,
max = axis.max * scale,
delta = (max - min) / options.noTicks,
ticks = [],
tickSize = axis.tickSize,
tickUnit,
formatter, i;
// Use custom formatter or time tick formatter
formatter = (options.tickFormatter === Flotr.defaultTickFormatter ?
this.formatter : options.tickFormatter
);
for (i = 0; i < spec.length - 1; ++i) {
var d = spec[i][0] * timeUnits[spec[i][1]];
if (delta < (d + spec[i+1][0] * timeUnits[spec[i+1][1]]) / 2 && d >= tickSize)
break;
}
tickSize = spec[i][0];
tickUnit = spec[i][1];
// special-case the possibility of several years
if (tickUnit == "year") {
tickSize = Flotr.getTickSize(options.noTicks*timeUnits.year, min, max, 0);
// Fix for 0.5 year case
if (tickSize == 0.5) {
tickUnit = "month";
tickSize = 6;
}
}
axis.tickUnit = tickUnit;
axis.tickSize = tickSize;
var step = tickSize * timeUnits[tickUnit];
d = new Date(min);
function setTick (name) {
set(d, name, mode, Flotr.floorInBase(
get(d, name, mode), tickSize
));
}
switch (tickUnit) {
case "millisecond": setTick('Milliseconds'); break;
case "second": setTick('Seconds'); break;
case "minute": setTick('Minutes'); break;
case "hour": setTick('Hours'); break;
case "month": setTick('Month'); break;
case "year": setTick('FullYear'); break;
}
// reset smaller components
if (step >= timeUnits.second) set(d, 'Milliseconds', mode, 0);
if (step >= timeUnits.minute) set(d, 'Seconds', mode, 0);
if (step >= timeUnits.hour) set(d, 'Minutes', mode, 0);
if (step >= timeUnits.day) set(d, 'Hours', mode, 0);
if (step >= timeUnits.day * 4) set(d, 'Date', mode, 1);
if (step >= timeUnits.year) set(d, 'Month', mode, 0);
var carry = 0, v = NaN, prev;
do {
prev = v;
v = d.getTime();
ticks.push({ v: v / scale, label: formatter(v / scale, axis) });
if (tickUnit == "month") {
if (tickSize < 1) {
/* a bit complicated - we'll divide the month up but we need to take care of fractions
so we don't end up in the middle of a day */
set(d, 'Date', mode, 1);
var start = d.getTime();
set(d, 'Month', mode, get(d, 'Month', mode) + 1);
var end = d.getTime();
d.setTime(v + carry * timeUnits.hour + (end - start) * tickSize);
carry = get(d, 'Hours', mode);
set(d, 'Hours', mode, 0);
}
else
set(d, 'Month', mode, get(d, 'Month', mode) + tickSize);
}
else if (tickUnit == "year") {
set(d, 'FullYear', mode, get(d, 'FullYear', mode) + tickSize);
}
else
d.setTime(v + step);
} while (v < max && v != prev);
return ticks;
},
timeUnits: {
millisecond: 1,
second: 1000,
minute: 1000 * 60,
hour: 1000 * 60 * 60,
day: 1000 * 60 * 60 * 24,
month: 1000 * 60 * 60 * 24 * 30,
year: 1000 * 60 * 60 * 24 * 365.2425
},
// the allowed tick sizes, after 1 year we use an integer algorithm
spec: [
[1, "millisecond"], [20, "millisecond"], [50, "millisecond"], [100, "millisecond"], [200, "millisecond"], [500, "millisecond"],
[1, "second"], [2, "second"], [5, "second"], [10, "second"], [30, "second"],
[1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], [30, "minute"],
[1, "hour"], [2, "hour"], [4, "hour"], [8, "hour"], [12, "hour"],
[1, "day"], [2, "day"], [3, "day"],
[0.25, "month"], [0.5, "month"], [1, "month"], [2, "month"], [3, "month"], [6, "month"],
[1, "year"]
],
monthNames: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
};
(function () {
var _ = Flotr._;
Flotr.DOM = {
addClass: function(element, name){
var classList = (element.className ? element.className : '');
if (_.include(classList.split(/\s+/g), name)) return;
element.className = (classList ? classList + ' ' : '') + name;
},
/**
* Create an element.
*/
create: function(tag){
return document.createElement(tag);
},
node: function(html) {
var div = Flotr.DOM.create('div'), n;
div.innerHTML = html;
n = div.children[0];
div.innerHTML = '';
return n;
},
/**
* Remove all children.
*/
empty: function(element){
element.innerHTML = '';
/*
if (!element) return;
_.each(element.childNodes, function (e) {
Flotr.DOM.empty(e);
element.removeChild(e);
});
*/
},
hide: function(element){
Flotr.DOM.setStyles(element, {display:'none'});
},
/**
* Insert a child.
* @param {Element} element
* @param {Element|String} Element or string to be appended.
*/
insert: function(element, child){
if(_.isString(child))
element.innerHTML += child;
else if (_.isElement(child))
element.appendChild(child);
},
// @TODO find xbrowser implementation
opacity: function(element, opacity) {
element.style.opacity = opacity;
},
position: function(element, p){
if (!element.offsetParent)
return {left: (element.offsetLeft || 0), top: (element.offsetTop || 0)};
p = this.position(element.offsetParent);
p.left += element.offsetLeft;
p.top += element.offsetTop;
return p;
},
removeClass: function(element, name) {
var classList = (element.className ? element.className : '');
element.className = _.filter(classList.split(/\s+/g), function (c) {
if (c != name) return true; }
).join(' ');
},
setStyles: function(element, o) {
_.each(o, function (value, key) {
element.style[key] = value;
});
},
show: function(element){
Flotr.DOM.setStyles(element, {display:''});
},
/**
* Return element size.
*/
size: function(element){
return {
height : element.offsetHeight,
width : element.offsetWidth };
}
};
})();
/**
* Flotr Event Adapter
*/
(function () {
var
F = Flotr,
bean = F.bean;
F.EventAdapter = {
observe: function(object, name, callback) {
bean.add(object, name, callback);
return this;
},
fire: function(object, name, args) {
bean.fire(object, name, args);
if (typeof(Prototype) != 'undefined')
Event.fire(object, name, args);
// @TODO Someone who uses mootools, add mootools adapter for existing applciations.
return this;
},
stopObserving: function(object, name, callback) {
bean.remove(object, name, callback);
return this;
},
eventPointer: function(e) {
if (!F._.isUndefined(e.touches) && e.touches.length > 0) {
return {
x : e.touches[0].pageX,
y : e.touches[0].pageY
};
} else if (!F._.isUndefined(e.changedTouches) && e.changedTouches.length > 0) {
return {
x : e.changedTouches[0].pageX,
y : e.changedTouches[0].pageY
};
} else if (e.pageX || e.pageY) {
return {
x : e.pageX,
y : e.pageY
};
} else if (e.clientX || e.clientY) {
var
d = document,
b = d.body,
de = d.documentElement;
return {
x: e.clientX + b.scrollLeft + de.scrollLeft,
y: e.clientY + b.scrollTop + de.scrollTop
};
}
}
};
})();
/**
* Flotr Graph class that plots a graph on creation.
*/
(function () {
var
D = Flotr.DOM,
E = Flotr.EventAdapter,
_ = Flotr._,
flotr = Flotr;
/**
* Flotr Graph constructor.
* @param {Element} el - element to insert the graph into
* @param {Object} data - an array or object of dataseries
* @param {Object} options - an object containing options
*/
Graph = function(el, data, options){
// Let's see if we can get away with out this [JS]
// try {
this._setEl(el);
this._initMembers();
this._initPlugins();
E.fire(this.el, 'flotr:beforeinit', [this]);
this.data = data;
this.series = flotr.Series.getSeries(data);
this._initOptions(options);
this._initGraphTypes();
this._initCanvas();
this._text = new flotr.Text({
element : this.el,
ctx : this.ctx,
html : this.options.HtmlText,
textEnabled : this.textEnabled
});
E.fire(this.el, 'flotr:afterconstruct', [this]);
this._initEvents();
this.findDataRanges();
this.calculateSpacing();
this.draw(_.bind(function() {
E.fire(this.el, 'flotr:afterinit', [this]);
}, this));
/*
try {
} catch (e) {
try {
console.error(e);
} catch (e2) {}
}*/
};
function observe (object, name, callback) {
E.observe.apply(this, arguments);
this._handles.push(arguments);
return this;
}
Graph.prototype = {
destroy: function () {
E.fire(this.el, 'flotr:destroy');
_.each(this._handles, function (handle) {
E.stopObserving.apply(this, handle);
});
this._handles = [];
this.el.graph = null;
},
observe : observe,
/**
* @deprecated
*/
_observe : observe,
processColor: function(color, options){
var o = { x1: 0, y1: 0, x2: this.plotWidth, y2: this.plotHeight, opacity: 1, ctx: this.ctx };
_.extend(o, options);
return flotr.Color.processColor(color, o);
},
/**
* Function determines the min and max values for the xaxis and yaxis.
*
* TODO logarithmic range validation (consideration of 0)
*/
findDataRanges: function(){
var a = this.axes,
xaxis, yaxis, range;
_.each(this.series, function (series) {
range = series.getRange();
if (range) {
xaxis = series.xaxis;
yaxis = series.yaxis;
xaxis.datamin = Math.min(range.xmin, xaxis.datamin);
xaxis.datamax = Math.max(range.xmax, xaxis.datamax);
yaxis.datamin = Math.min(range.ymin, yaxis.datamin);
yaxis.datamax = Math.max(range.ymax, yaxis.datamax);
xaxis.used = (xaxis.used || range.xused);
yaxis.used = (yaxis.used || range.yused);
}
}, this);
// Check for empty data, no data case (none used)
if (!a.x.used && !a.x2.used) a.x.used = true;
if (!a.y.used && !a.y2.used) a.y.used = true;
_.each(a, function (axis) {
axis.calculateRange();
});
var
types = _.keys(flotr.graphTypes),
drawn = false;
_.each(this.series, function (series) {
if (series.hide) return;
_.each(types, function (type) {
if (series[type] && series[type].show) {
this.extendRange(type, series);
drawn = true;
}
}, this);
if (!drawn) {
this.extendRange(this.options.defaultType, series);
}
}, this);
},
extendRange : function (type, series) {
if (this[type].extendRange) this[type].extendRange(series, series.data, series[type], this[type]);
if (this[type].extendYRange) this[type].extendYRange(series.yaxis, series.data, series[type], this[type]);
if (this[type].extendXRange) this[type].extendXRange(series.xaxis, series.data, series[type], this[type]);
},
/**
* Calculates axis label sizes.
*/
calculateSpacing: function(){
var a = this.axes,
options = this.options,
series = this.series,
margin = options.grid.labelMargin,
T = this._text,
x = a.x,
x2 = a.x2,
y = a.y,
y2 = a.y2,
maxOutset = options.grid.outlineWidth,
i, j, l, dim;
// TODO post refactor, fix this
_.each(a, function (axis) {
axis.calculateTicks();
axis.calculateTextDimensions(T, options);
});
// Title height
dim = T.dimensions(
options.title,
{size: options.fontSize*1.5},
'font-size:1em;font-weight:bold;',
'flotr-title'
);
this.titleHeight = dim.height;
// Subtitle height
dim = T.dimensions(
options.subtitle,
{size: options.fontSize},
'font-size:smaller;',
'flotr-subtitle'
);
this.subtitleHeight = dim.height;
for(j = 0; j < options.length; ++j){
if (series[j].points.show){
maxOutset = Math.max(maxOutset, series[j].points.radius + series[j].points.lineWidth/2);
}
}
var p = this.plotOffset;
if (x.options.margin === false) {
p.bottom = 0;
p.top = 0;
} else {
p.bottom += (options.grid.circular ? 0 : (x.used && x.options.showLabels ? (x.maxLabel.height + margin) : 0)) +
(x.used && x.options.title ? (x.titleSize.height + margin) : 0) + maxOutset;
p.top += (options.grid.circular ? 0 : (x2.used && x2.options.showLabels ? (x2.maxLabel.height + margin) : 0)) +
(x2.used && x2.options.title ? (x2.titleSize.height + margin) : 0) + this.subtitleHeight + this.titleHeight + maxOutset;
}
if (y.options.margin === false) {
p.left = 0;
p.right = 0;
} else {
p.left += (options.grid.circular ? 0 : (y.used && y.options.showLabels ? (y.maxLabel.width + margin) : 0)) +
(y.used && y.options.title ? (y.titleSize.width + margin) : 0) + maxOutset;
p.right += (options.grid.circular ? 0 : (y2.used && y2.options.showLabels ? (y2.maxLabel.width + margin) : 0)) +
(y2.used && y2.options.title ? (y2.titleSize.width + margin) : 0) + maxOutset;
}
p.top = Math.floor(p.top); // In order the outline not to be blured
this.plotWidth = this.canvasWidth - p.left - p.right;
this.plotHeight = this.canvasHeight - p.bottom - p.top;
// TODO post refactor, fix this
x.length = x2.length = this.plotWidth;
y.length = y2.length = this.plotHeight;
y.offset = y2.offset = this.plotHeight;
x.setScale();
x2.setScale();
y.setScale();
y2.setScale();
},
/**
* Draws grid, labels, series and outline.
*/
draw: function(after) {
var
context = this.ctx,
i;
E.fire(this.el, 'flotr:beforedraw', [this.series, this]);
if (this.series.length) {
context.save();
context.translate(this.plotOffset.left, this.plotOffset.top);
for (i = 0; i < this.series.length; i++) {
if (!this.series[i].hide) this.drawSeries(this.series[i]);
}
context.restore();
this.clip();
}
E.fire(this.el, 'flotr:afterdraw', [this.series, this]);
if (after) after();
},
/**
* Actually draws the graph.
* @param {Object} series - series to draw
*/
drawSeries: function(series){
function drawChart (series, typeKey) {
var options = this.getOptions(series, typeKey);
this[typeKey].draw(options);
}
var drawn = false;
series = series || this.series;
_.each(flotr.graphTypes, function (type, typeKey) {
if (series[typeKey] && series[typeKey].show && this[typeKey]) {
drawn = true;
drawChart.call(this, series, typeKey);
}
}, this);
if (!drawn) drawChart.call(this, series, this.options.defaultType);
},
getOptions : function (series, typeKey) {
var
type = series[typeKey],
graphType = this[typeKey],
xaxis = series.xaxis,
yaxis = series.yaxis,
options = {
context : this.ctx,
width : this.plotWidth,
height : this.plotHeight,
fontSize : this.options.fontSize,
fontColor : this.options.fontColor,
textEnabled : this.textEnabled,
htmlText : this.options.HtmlText,
text : this._text, // TODO Is this necessary?
element : this.el,
data : series.data,
color : series.color,
shadowSize : series.shadowSize,
xScale : xaxis.d2p,
yScale : yaxis.d2p,
xInverse : xaxis.p2d,
yInverse : yaxis.p2d
};
options = flotr.merge(type, options);
// Fill
options.fillStyle = this.processColor(
type.fillColor || series.color,
{opacity: type.fillOpacity}
);
return options;
},
/**
* Calculates the coordinates from a mouse event object.
* @param {Event} event - Mouse Event object.
* @return {Object} Object with coordinates of the mouse.
*/
getEventPosition: function (e){
var
d = document,
b = d.body,
de = d.documentElement,
axes = this.axes,
plotOffset = this.plotOffset,
lastMousePos = this.lastMousePos,
pointer = E.eventPointer(e),
dx = pointer.x - lastMousePos.pageX,
dy = pointer.y - lastMousePos.pageY,
r, rx, ry;
if ('ontouchstart' in this.el) {
r = D.position(this.overlay);
rx = pointer.x - r.left - plotOffset.left;
ry = pointer.y - r.top - plotOffset.top;
} else {
r = this.overlay.getBoundingClientRect();
rx = e.clientX - r.left - plotOffset.left - b.scrollLeft - de.scrollLeft;
ry = e.clientY - r.top - plotOffset.top - b.scrollTop - de.scrollTop;
}
return {
x: axes.x.p2d(rx),
x2: axes.x2.p2d(rx),
y: axes.y.p2d(ry),
y2: axes.y2.p2d(ry),
relX: rx,
relY: ry,
dX: dx,
dY: dy,
absX: pointer.x,
absY: pointer.y,
pageX: pointer.x,
pageY: pointer.y
};
},
/**
* Observes the 'click' event and fires the 'flotr:click' event.
* @param {Event} event - 'click' Event object.
*/
clickHandler: function(event){
if(this.ignoreClick){
this.ignoreClick = false;
return this.ignoreClick;
}
E.fire(this.el, 'flotr:click', [this.getEventPosition(event), this]);
},
/**
* Observes mouse movement over the graph area. Fires the 'flotr:mousemove' event.
* @param {Event} event - 'mousemove' Event object.
*/
mouseMoveHandler: function(event){
if (this.mouseDownMoveHandler) return;
var pos = this.getEventPosition(event);
E.fire(this.el, 'flotr:mousemove', [event, pos, this]);
this.lastMousePos = pos;
},
/**
* Observes the 'mousedown' event.
* @param {Event} event - 'mousedown' Event object.
*/
mouseDownHandler: function (event){
/*
// @TODO Context menu?
if(event.isRightClick()) {
event.stop();
var overlay = this.overlay;
overlay.hide();
function cancelContextMenu () {
overlay.show();
E.stopObserving(document, 'mousemove', cancelContextMenu);
}
E.observe(document, 'mousemove', cancelContextMenu);
return;
}
*/
if (this.mouseUpHandler) return;
this.mouseUpHandler = _.bind(function (e) {
E.stopObserving(document, 'mouseup', this.mouseUpHandler);
E.stopObserving(document, 'mousemove', this.mouseDownMoveHandler);
this.mouseDownMoveHandler = null;
this.mouseUpHandler = null;
// @TODO why?
//e.stop();
E.fire(this.el, 'flotr:mouseup', [e, this]);
}, this);
this.mouseDownMoveHandler = _.bind(function (e) {
var pos = this.getEventPosition(e);
E.fire(this.el, 'flotr:mousemove', [event, pos, this]);
this.lastMousePos = pos;
}, this);
E.observe(document, 'mouseup', this.mouseUpHandler);
E.observe(document, 'mousemove', this.mouseDownMoveHandler);
E.fire(this.el, 'flotr:mousedown', [event, this]);
this.ignoreClick = false;
},
drawTooltip: function(content, x, y, options) {
var mt = this.getMouseTrack(),
style = 'opacity:0.7;background-color:#000;color:#fff;display:none;position:absolute;padding:2px 8px;-moz-border-radius:4px;border-radius:4px;white-space:nowrap;',
p = options.position,
m = options.margin,
plotOffset = this.plotOffset;
if(x !== null && y !== null){
if (!options.relative) { // absolute to the canvas
if(p.charAt(0) == 'n') style += 'top:' + (m + plotOffset.top) + 'px;bottom:auto;';
else if(p.charAt(0) == 's') style += 'bottom:' + (m + plotOffset.bottom) + 'px;top:auto;';
if(p.charAt(1) == 'e') style += 'right:' + (m + plotOffset.right) + 'px;left:auto;';
else if(p.charAt(1) == 'w') style += 'left:' + (m + plotOffset.left) + 'px;right:auto;';
}
else { // relative to the mouse
if(p.charAt(0) == 'n') style += 'bottom:' + (m - plotOffset.top - y + this.canvasHeight) + 'px;top:auto;';
else if(p.charAt(0) == 's') style += 'top:' + (m + plotOffset.top + y) + 'px;bottom:auto;';
if(p.charAt(1) == 'e') style += 'left:' + (m + plotOffset.left + x) + 'px;right:auto;';
else if(p.charAt(1) == 'w') style += 'right:' + (m - plotOffset.left - x + this.canvasWidth) + 'px;left:auto;';
}
mt.style.cssText = style;
D.empty(mt);
D.insert(mt, content);
D.show(mt);
}
else {
D.hide(mt);
}
},
clip: function (ctx) {
var
o = this.plotOffset,
w = this.canvasWidth,
h = this.canvasHeight;
ctx = ctx || this.ctx;
if (flotr.isIE && flotr.isIE < 9) {
// Clipping for excanvas :-(
ctx.save();
ctx.fillStyle = this.processColor(this.options.ieBackgroundColor);
ctx.fillRect(0, 0, w, o.top);
ctx.fillRect(0, 0, o.left, h);
ctx.fillRect(0, h - o.bottom, w, o.bottom);
ctx.fillRect(w - o.right, 0, o.right,h);
ctx.restore();
} else {
ctx.clearRect(0, 0, w, o.top);
ctx.clearRect(0, 0, o.left, h);
ctx.clearRect(0, h - o.bottom, w, o.bottom);
ctx.clearRect(w - o.right, 0, o.right,h);
}
},
_initMembers: function() {
this._handles = [];
this.lastMousePos = {pageX: null, pageY: null };
this.plotOffset = {left: 0, right: 0, top: 0, bottom: 0};
this.ignoreClick = true;
this.prevHit = null;
},
_initGraphTypes: function() {
_.each(flotr.graphTypes, function(handler, graphType){
this[graphType] = flotr.clone(handler);
}, this);
},
_initEvents: function () {
var
el = this.el,
touchendHandler, movement, touchend;
if ('ontouchstart' in el) {
touchendHandler = _.bind(function (e) {
touchend = true;
E.stopObserving(document, 'touchend', touchendHandler);
E.fire(el, 'flotr:mouseup', [event, this]);
this.multitouches = null;
if (!movement) {
this.clickHandler(e);
}
}, this);
this.observe(this.overlay, 'touchstart', _.bind(function (e) {
movement = false;
touchend = false;
this.ignoreClick = false;
if (e.touches && e.touches.length > 1) {
this.multitouches = e.touches;
}
E.fire(el, 'flotr:mousedown', [event, this]);
this.observe(document, 'touchend', touchendHandler);
}, this));
this.observe(this.overlay, 'touchmove', _.bind(function (e) {
var pos = this.getEventPosition(e);
if (this.options.preventDefault) {
e.preventDefault();
}
movement = true;
if (this.multitouches || (e.touches && e.touches.length > 1)) {
this.multitouches = e.touches;
} else {
if (!touchend) {
E.fire(el, 'flotr:mousemove', [event, pos, this]);
}
}
this.lastMousePos = pos;
}, this));
} else {
this.
observe(this.overlay, 'mousedown', _.bind(this.mouseDownHandler, this)).
observe(el, 'mousemove', _.bind(this.mouseMoveHandler, this)).
observe(this.overlay, 'click', _.bind(this.clickHandler, this)).
observe(el, 'mouseout', function () {
E.fire(el, 'flotr:mouseout');
});
}
},
/**
* Initializes the canvas and it's overlay canvas element. When the browser is IE, this makes use
* of excanvas. The overlay canvas is inserted for displaying interactions. After the canvas elements
* are created, the elements are inserted into the container element.
*/
_initCanvas: function(){
var el = this.el,
o = this.options,
children = el.children,
removedChildren = [],
child, i,
size, style;
// Empty the el
for (i = children.length; i--;) {
child = children[i];
if (!this.canvas && child.className === 'flotr-canvas') {
this.canvas = child;
} else if (!this.overlay && child.className === 'flotr-overlay') {
this.overlay = child;
} else {
removedChildren.push(child);
}
}
for (i = removedChildren.length; i--;) {
el.removeChild(removedChildren[i]);
}
D.setStyles(el, {position: 'relative'}); // For positioning labels and overlay.
size = {};
size.width = el.clientWidth;
size.height = el.clientHeight;
if(size.width <= 0 || size.height <= 0 || o.resolution <= 0){
throw 'Invalid dimensions for plot, width = ' + size.width + ', height = ' + size.height + ', resolution = ' + o.resolution;
}
// Main canvas for drawing graph types
this.canvas = getCanvas(this.canvas, 'canvas');
// Overlay canvas for interactive features
this.overlay = getCanvas(this.overlay, 'overlay');
this.ctx = getContext(this.canvas);
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.octx = getContext(this.overlay);
this.octx.clearRect(0, 0, this.overlay.width, this.overlay.height);
this.canvasHeight = size.height;
this.canvasWidth = size.width;
this.textEnabled = !!this.ctx.drawText || !!this.ctx.fillText; // Enable text functions
function getCanvas(canvas, name){
if(!canvas){
canvas = D.create('canvas');
if (typeof FlashCanvas != "undefined" && typeof canvas.getContext === 'function') {
FlashCanvas.initElement(canvas);
}
canvas.className = 'flotr-'+name;
canvas.style.cssText = 'position:absolute;left:0px;top:0px;';
D.insert(el, canvas);
}
_.each(size, function(size, attribute){
D.show(canvas);
if (name == 'canvas' && canvas.getAttribute(attribute) === size) {
return;
}
canvas.setAttribute(attribute, size * o.resolution);
canvas.style[attribute] = size + 'px';
});
canvas.context_ = null; // Reset the ExCanvas context
return canvas;
}
function getContext(canvas){
if(window.G_vmlCanvasManager) window.G_vmlCanvasManager.initElement(canvas); // For ExCanvas
var context = canvas.getContext('2d');
if(!window.G_vmlCanvasManager) context.scale(o.resolution, o.resolution);
return context;
}
},
_initPlugins: function(){
// TODO Should be moved to flotr and mixed in.
_.each(flotr.plugins, function(plugin, name){
_.each(plugin.callbacks, function(fn, c){
this.observe(this.el, c, _.bind(fn, this));
}, this);
this[name] = flotr.clone(plugin);
_.each(this[name], function(fn, p){
if (_.isFunction(fn))
this[name][p] = _.bind(fn, this);
}, this);
}, this);
},
/**
* Sets options and initializes some variables and color specific values, used by the constructor.
* @param {Object} opts - options object
*/
_initOptions: function(opts){
var options = flotr.clone(flotr.defaultOptions);
options.x2axis = _.extend(_.clone(options.xaxis), options.x2axis);
options.y2axis = _.extend(_.clone(options.yaxis), options.y2axis);
this.options = flotr.merge(opts || {}, options);
if (this.options.grid.minorVerticalLines === null &&
this.options.xaxis.scaling === 'logarithmic') {
this.options.grid.minorVerticalLines = true;
}
if (this.options.grid.minorHorizontalLines === null &&
this.options.yaxis.scaling === 'logarithmic') {
this.options.grid.minorHorizontalLines = true;
}
E.fire(this.el, 'flotr:afterinitoptions', [this]);
this.axes = flotr.Axis.getAxes(this.options);
// Initialize some variables used throughout this function.
var assignedColors = [],
colors = [],
ln = this.series.length,
neededColors = this.series.length,
oc = this.options.colors,
usedColors = [],
variation = 0,
c, i, j, s;
// Collect user-defined colors from series.
for(i = neededColors - 1; i > -1; --i){
c = this.series[i].color;
if(c){
--neededColors;
if(_.isNumber(c)) assignedColors.push(c);
else usedColors.push(flotr.Color.parse(c));
}
}
// Calculate the number of colors that need to be generated.
for(i = assignedColors.length - 1; i > -1; --i)
neededColors = Math.max(neededColors, assignedColors[i] + 1);
// Generate needed number of colors.
for(i = 0; colors.length < neededColors;){
c = (oc.length == i) ? new flotr.Color(100, 100, 100) : flotr.Color.parse(oc[i]);
// Make sure each serie gets a different color.
var sign = variation % 2 == 1 ? -1 : 1,
factor = 1 + sign * Math.ceil(variation / 2) * 0.2;
c.scale(factor, factor, factor);
/**
* @todo if we're getting too close to something else, we should probably skip this one
*/
colors.push(c);
if(++i >= oc.length){
i = 0;
++variation;
}
}
// Fill the options with the generated colors.
for(i = 0, j = 0; i < ln; ++i){
s = this.series[i];
// Assign the color.
if (!s.color){
s.color = colors[j++].toString();
}else if(_.isNumber(s.color)){
s.color = colors[s.color].toString();
}
// Every series needs an axis
if (!s.xaxis) s.xaxis = this.axes.x;
if (s.xaxis == 1) s.xaxis = this.axes.x;
else if (s.xaxis == 2) s.xaxis = this.axes.x2;
if (!s.yaxis) s.yaxis = this.axes.y;
if (s.yaxis == 1) s.yaxis = this.axes.y;
else if (s.yaxis == 2) s.yaxis = this.axes.y2;
// Apply missing options to the series.
for (var t in flotr.graphTypes){
s[t] = _.extend(_.clone(this.options[t]), s[t]);
}
s.mouse = _.extend(_.clone(this.options.mouse), s.mouse);
if (_.isUndefined(s.shadowSize)) s.shadowSize = this.options.shadowSize;
}
},
_setEl: function(el) {
if (!el) throw 'The target container doesn\'t exist';
else if (el.graph instanceof Graph) el.graph.destroy();
else if (!el.clientWidth) throw 'The target container must be visible';
el.graph = this;
this.el = el;
}
};
Flotr.Graph = Graph;
})();
/**
* Flotr Axis Library
*/
(function () {
var
_ = Flotr._,
LOGARITHMIC = 'logarithmic';
function Axis (o) {
this.orientation = 1;
this.offset = 0;
this.datamin = Number.MAX_VALUE;
this.datamax = -Number.MAX_VALUE;
_.extend(this, o);
}
// Prototype
Axis.prototype = {
setScale : function () {
var
length = this.length,
max = this.max,
min = this.min,
offset = this.offset,
orientation = this.orientation,
options = this.options,
logarithmic = options.scaling === LOGARITHMIC,
scale;
if (logarithmic) {
scale = length / (log(max, options.base) - log(min, options.base));
} else {
scale = length / (max - min);
}
this.scale = scale;
// Logarithmic?
if (logarithmic) {
this.d2p = function (dataValue) {
return offset + orientation * (log(dataValue, options.base) - log(min, options.base)) * scale;
}
this.p2d = function (pointValue) {
return exp((offset + orientation * pointValue) / scale + log(min, options.base), options.base);
}
} else {
this.d2p = function (dataValue) {
return offset + orientation * (dataValue - min) * scale;
}
this.p2d = function (pointValue) {
return (offset + orientation * pointValue) / scale + min;
}
}
},
calculateTicks : function () {
var options = this.options;
this.ticks = [];
this.minorTicks = [];
// User Ticks
if(options.ticks){
this._cleanUserTicks(options.ticks, this.ticks);
this._cleanUserTicks(options.minorTicks || [], this.minorTicks);
}
else {
if (options.mode == 'time') {
this._calculateTimeTicks();
} else if (options.scaling === 'logarithmic') {
this._calculateLogTicks();
} else {
this._calculateTicks();
}
}
// Ticks to strings
_.each(this.ticks, function (tick) { tick.label += ''; });
_.each(this.minorTicks, function (tick) { tick.label += ''; });
},
/**
* Calculates the range of an axis to apply autoscaling.
*/
calculateRange: function () {
if (!this.used) return;
var axis = this,
o = axis.options,
min = o.min !== null ? o.min : axis.datamin,
max = o.max !== null ? o.max : axis.datamax,
margin = o.autoscaleMargin;
if (o.scaling == 'logarithmic') {
if (min <= 0) min = axis.datamin;
// Let it widen later on
if (max <= 0) max = min;
}
if (max == min) {
var widen = max ? 0.01 : 1.00;
if (o.min === null) min -= widen;
if (o.max === null) max += widen;
}
if (o.scaling === 'logarithmic') {
if (min < 0) min = max / o.base; // Could be the result of widening
var maxexp = Math.log(max);
if (o.base != Math.E) maxexp /= Math.log(o.base);
maxexp = Math.ceil(maxexp);
var minexp = Math.log(min);
if (o.base != Math.E) minexp /= Math.log(o.base);
minexp = Math.ceil(minexp);
axis.tickSize = Flotr.getTickSize(o.noTicks, minexp, maxexp, o.tickDecimals === null ? 0 : o.tickDecimals);
// Try to determine a suitable amount of miniticks based on the length of a decade
if (o.minorTickFreq === null) {
if (maxexp - minexp > 10)
o.minorTickFreq = 0;
else if (maxexp - minexp > 5)
o.minorTickFreq = 2;
else
o.minorTickFreq = 5;
}
} else {
axis.tickSize = Flotr.getTickSize(o.noTicks, min, max, o.tickDecimals);
}
axis.min = min;
axis.max = max; //extendRange may use axis.min or axis.max, so it should be set before it is caled
// Autoscaling. @todo This probably fails with log scale. Find a testcase and fix it
if(o.min === null && o.autoscale){
axis.min -= axis.tickSize * margin;
// Make sure we don't go below zero if all values are positive.
if(axis.min < 0 && axis.datamin >= 0) axis.min = 0;
axis.min = axis.tickSize * Math.floor(axis.min / axis.tickSize);
}
if(o.max === null && o.autoscale){
axis.max += axis.tickSize * margin;
if(axis.max > 0 && axis.datamax <= 0 && axis.datamax != axis.datamin) axis.max = 0;
axis.max = axis.tickSize * Math.ceil(axis.max / axis.tickSize);
}
if (axis.min == axis.max) axis.max = axis.min + 1;
},
calculateTextDimensions : function (T, options) {
var maxLabel = '',
length,
i;
if (this.options.showLabels) {
for (i = 0; i < this.ticks.length; ++i) {
length = this.ticks[i].label.length;
if (length > maxLabel.length){
maxLabel = this.ticks[i].label;
}
}
}
this.maxLabel = T.dimensions(
maxLabel,
{size:options.fontSize, angle: Flotr.toRad(this.options.labelsAngle)},
'font-size:smaller;',
'flotr-grid-label'
);
this.titleSize = T.dimensions(
this.options.title,
{size:options.fontSize*1.2, angle: Flotr.toRad(this.options.titleAngle)},
'font-weight:bold;',
'flotr-axis-title'
);
},
_cleanUserTicks : function (ticks, axisTicks) {
var axis = this, options = this.options,
v, i, label, tick;
if(_.isFunction(ticks)) ticks = ticks({min : axis.min, max : axis.max});
for(i = 0; i < ticks.length; ++i){
tick = ticks[i];
if(typeof(tick) === 'object'){
v = tick[0];
label = (tick.length > 1) ? tick[1] : options.tickFormatter(v, {min : axis.min, max : axis.max});
} else {
v = tick;
label = options.tickFormatter(v, {min : this.min, max : this.max});
}
axisTicks[i] = { v: v, label: label };
}
},
_calculateTimeTicks : function () {
this.ticks = Flotr.Date.generator(this);
},
_calculateLogTicks : function () {
var axis = this,
o = axis.options,
v,
decadeStart;
var max = Math.log(axis.max);
if (o.base != Math.E) max /= Math.log(o.base);
max = Math.ceil(max);
var min = Math.log(axis.min);
if (o.base != Math.E) min /= Math.log(o.base);
min = Math.ceil(min);
for (i = min; i < max; i += axis.tickSize) {
decadeStart = (o.base == Math.E) ? Math.exp(i) : Math.pow(o.base, i);
// Next decade begins here:
var decadeEnd = decadeStart * ((o.base == Math.E) ? Math.exp(axis.tickSize) : Math.pow(o.base, axis.tickSize));
var stepSize = (decadeEnd - decadeStart) / o.minorTickFreq;
axis.ticks.push({v: decadeStart, label: o.tickFormatter(decadeStart, {min : axis.min, max : axis.max})});
for (v = decadeStart + stepSize; v < decadeEnd; v += stepSize)
axis.minorTicks.push({v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max})});
}
// Always show the value at the would-be start of next decade (end of this decade)
decadeStart = (o.base == Math.E) ? Math.exp(i) : Math.pow(o.base, i);
axis.ticks.push({v: decadeStart, label: o.tickFormatter(decadeStart, {min : axis.min, max : axis.max})});
},
_calculateTicks : function () {
var axis = this,
o = axis.options,
tickSize = axis.tickSize,
min = axis.min,
max = axis.max,
start = tickSize * Math.ceil(min / tickSize), // Round to nearest multiple of tick size.
decimals,
minorTickSize,
v, v2,
i, j;
if (o.minorTickFreq)
minorTickSize = tickSize / o.minorTickFreq;
// Then store all possible ticks.
for (i = 0; (v = v2 = start + i * tickSize) <= max; ++i){
// Round (this is always needed to fix numerical instability).
decimals = o.tickDecimals;
if (decimals === null) decimals = 1 - Math.floor(Math.log(tickSize) / Math.LN10);
if (decimals < 0) decimals = 0;
v = v.toFixed(decimals);
axis.ticks.push({ v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max}) });
if (o.minorTickFreq) {
for (j = 0; j < o.minorTickFreq && (i * tickSize + j * minorTickSize) < max; ++j) {
v = v2 + j * minorTickSize;
axis.minorTicks.push({ v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max}) });
}
}
}
}
};
// Static Methods
_.extend(Axis, {
getAxes : function (options) {
return {
x: new Axis({options: options.xaxis, n: 1, length: this.plotWidth}),
x2: new Axis({options: options.x2axis, n: 2, length: this.plotWidth}),
y: new Axis({options: options.yaxis, n: 1, length: this.plotHeight, offset: this.plotHeight, orientation: -1}),
y2: new Axis({options: options.y2axis, n: 2, length: this.plotHeight, offset: this.plotHeight, orientation: -1})
};
}
});
// Helper Methods
function log (value, base) {
value = Math.log(Math.max(value, Number.MIN_VALUE));
if (base !== Math.E)
value /= Math.log(base);
return value;
}
function exp (value, base) {
return (base === Math.E) ? Math.exp(value) : Math.pow(base, value);
}
Flotr.Axis = Axis;
})();
/**
* Flotr Series Library
*/
(function () {
var
_ = Flotr._;
function Series (o) {
_.extend(this, o);
}
Series.prototype = {
getRange: function () {
var
data = this.data,
length = data.length,
xmin = Number.MAX_VALUE,
ymin = Number.MAX_VALUE,
xmax = -Number.MAX_VALUE,
ymax = -Number.MAX_VALUE,
xused = false,
yused = false,
x, y, i;
if (length < 0 || this.hide) return false;
for (i = 0; i < length; i++) {
x = data[i][0];
y = data[i][1];
if (x !== null) {
if (x < xmin) { xmin = x; xused = true; }
if (x > xmax) { xmax = x; xused = true; }
}
if (y !== null) {
if (y < ymin) { ymin = y; yused = true; }
if (y > ymax) { ymax = y; yused = true; }
}
}
return {
xmin : xmin,
xmax : xmax,
ymin : ymin,
ymax : ymax,
xused : xused,
yused : yused
};
}
};
_.extend(Series, {
/**
* Collects dataseries from input and parses the series into the right format. It returns an Array
* of Objects each having at least the 'data' 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;
})();
/**
* Text Utilities
*/
(function () {
var
F = Flotr,
D = F.DOM,
_ = F._,
Text = function (o) {
this.o = o;
};
Text.prototype = {
dimensions : function (text, canvasStyle, htmlStyle, className) {
if (!text) return { width : 0, height : 0 };
return (this.o.html) ?
this.html(text, this.o.element, htmlStyle, className) :
this.canvas(text, canvasStyle);
},
canvas : function (text, style) {
if (!this.o.textEnabled) return;
style = style || {};
var
metrics = this.measureText(text, style),
width = metrics.width,
height = style.size || F.defaultOptions.fontSize,
angle = style.angle || 0,
cosAngle = Math.cos(angle),
sinAngle = Math.sin(angle),
widthPadding = 2,
heightPadding = 6,
bounds;
bounds = {
width: Math.abs(cosAngle * width) + Math.abs(sinAngle * height) + widthPadding,
height: Math.abs(sinAngle * width) + Math.abs(cosAngle * height) + heightPadding
};
return bounds;
},
html : function (text, element, style, className) {
var div = D.create('div');
D.setStyles(div, { 'position' : 'absolute', 'top' : '-10000px' });
D.insert(div, '' + text + '');
D.insert(this.o.element, div);
return D.size(div);
},
measureText : function (text, style) {
var
context = this.o.ctx,
metrics;
if (!context.fillText || (F.isIphone && context.measure)) {
return { width : context.measure(text, style)};
}
style = _.extend({
size: F.defaultOptions.fontSize,
weight: 1,
angle: 0
}, style);
context.save();
context.font = (style.weight > 1 ? "bold " : "") + (style.size*1.3) + "px sans-serif";
metrics = context.measureText(text);
context.restore();
return metrics;
}
};
Flotr.Text = Text;
})();
/** Lines **/
Flotr.addType('lines', {
options: {
show: false, // => setting to true will show lines, false will hide
lineWidth: 2, // => line width in pixels
fill: false, // => true to fill the area from the line to the x axis, false for (transparent) no fill
fillBorder: false, // => draw a border around the fill
fillColor: null, // => fill color
fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
steps: false, // => draw steps
stacked: false // => setting to true will show stacked lines, false will show normal lines
},
stack : {
values : []
},
/**
* Draws lines series in the canvas element.
* @param {Object} options
*/
draw : function (options) {
var
context = options.context,
lineWidth = options.lineWidth,
shadowSize = options.shadowSize,
offset;
context.save();
context.lineJoin = 'round';
if (shadowSize) {
context.lineWidth = shadowSize / 2;
offset = lineWidth / 2 + context.lineWidth / 2;
// @TODO do this instead with a linear gradient
context.strokeStyle = "rgba(0,0,0,0.1)";
this.plot(options, offset + shadowSize / 2, false);
context.strokeStyle = "rgba(0,0,0,0.2)";
this.plot(options, offset, false);
}
context.lineWidth = lineWidth;
context.strokeStyle = options.color;
this.plot(options, 0, true);
context.restore();
},
plot : function (options, shadowOffset, incStack) {
var
context = options.context,
width = options.width,
height = options.height,
xScale = options.xScale,
yScale = options.yScale,
data = options.data,
stack = options.stacked ? this.stack : false,
length = data.length - 1,
prevx = null,
prevy = null,
zero = yScale(0),
start = null,
x1, x2, y1, y2, stack1, stack2, i;
if (length < 1) return;
context.beginPath();
for (i = 0; i < length; ++i) {
// To allow empty values
if (data[i][1] === null || data[i+1][1] === null) {
if (options.fill) {
if (i > 0 && data[i][1]) {
context.stroke();
fill();
start = null;
context.closePath();
context.beginPath();
}
}
continue;
}
// Zero is infinity for log scales
// TODO handle zero for logarithmic
// if (xa.options.scaling === 'logarithmic' && (data[i][0] <= 0 || data[i+1][0] <= 0)) continue;
// if (ya.options.scaling === 'logarithmic' && (data[i][1] <= 0 || data[i+1][1] <= 0)) continue;
x1 = xScale(data[i][0]);
x2 = xScale(data[i+1][0]);
if (start === null) start = data[i];
if (stack) {
stack1 = stack.values[data[i][0]] || 0;
stack2 = stack.values[data[i+1][0]] || stack.values[data[i][0]] || 0;
y1 = yScale(data[i][1] + stack1);
y2 = yScale(data[i+1][1] + stack2);
if(incStack){
stack.values[data[i][0]] = data[i][1]+stack1;
if(i == length-1)
stack.values[data[i+1][0]] = data[i+1][1]+stack2;
}
}
else{
y1 = yScale(data[i][1]);
y2 = yScale(data[i+1][1]);
}
if (
(y1 > height && y2 > height) ||
(y1 < 0 && y2 < 0) ||
(x1 < 0 && x2 < 0) ||
(x1 > width && x2 > width)
) continue;
if((prevx != x1) || (prevy != y1 + shadowOffset))
context.moveTo(x1, y1 + shadowOffset);
prevx = x2;
prevy = y2 + shadowOffset;
if (options.steps) {
context.lineTo(prevx + shadowOffset / 2, y1 + shadowOffset);
context.lineTo(prevx + shadowOffset / 2, prevy);
} else {
context.lineTo(prevx, prevy);
}
}
if (!options.fill || options.fill && !options.fillBorder) context.stroke();
fill();
function fill () {
// TODO stacked lines
if(!shadowOffset && options.fill && start){
x1 = xScale(start[0]);
context.fillStyle = options.fillStyle;
context.lineTo(x2, zero);
context.lineTo(x1, zero);
context.lineTo(x1, yScale(start[1]));
context.fill();
if (options.fillBorder) {
context.stroke();
}
}
}
context.closePath();
},
// Perform any pre-render precalculations (this should be run on data first)
// - Pie chart total for calculating measures
// - Stacks for lines and bars
// precalculate : function () {
// }
//
//
// Get any bounds after pre calculation (axis can fetch this if does not have explicit min/max)
// getBounds : function () {
// }
// getMin : function () {
// }
// getMax : function () {
// }
//
//
// Padding around rendered elements
// getPadding : function () {
// }
extendYRange : function (axis, data, options, lines) {
var o = axis.options;
// If stacked and auto-min
if (options.stacked && ((!o.max && o.max !== 0) || (!o.min && o.min !== 0))) {
var
newmax = axis.max,
newmin = axis.min,
positiveSums = lines.positiveSums || {},
negativeSums = lines.negativeSums || {},
x, j;
for (j = 0; j < data.length; j++) {
x = data[j][0] + '';
// Positive
if (data[j][1] > 0) {
positiveSums[x] = (positiveSums[x] || 0) + data[j][1];
newmax = Math.max(newmax, positiveSums[x]);
}
// Negative
else {
negativeSums[x] = (negativeSums[x] || 0) + data[j][1];
newmin = Math.min(newmin, negativeSums[x]);
}
}
lines.negativeSums = negativeSums;
lines.positiveSums = positiveSums;
axis.max = newmax;
axis.min = newmin;
}
if (options.steps) {
this.hit = function (options) {
var
data = options.data,
args = options.args,
yScale = options.yScale,
mouse = args[0],
length = data.length,
n = args[1],
x = options.xInverse(mouse.relX),
relY = mouse.relY,
i;
for (i = 0; i < length - 1; i++) {
if (x >= data[i][0] && x <= data[i+1][0]) {
if (Math.abs(yScale(data[i][1]) - relY) < 8) {
n.x = data[i][0];
n.y = data[i][1];
n.index = i;
n.seriesIndex = options.index;
}
break;
}
}
};
this.drawHit = function (options) {
var
context = options.context,
args = options.args,
data = options.data,
xScale = options.xScale,
index = args.index,
x = xScale(args.x),
y = options.yScale(args.y),
x2;
if (data.length - 1 > index) {
x2 = options.xScale(data[index + 1][0]);
context.save();
context.strokeStyle = options.color;
context.lineWidth = options.lineWidth;
context.beginPath();
context.moveTo(x, y);
context.lineTo(x2, y);
context.stroke();
context.closePath();
context.restore();
}
};
this.clearHit = function (options) {
var
context = options.context,
args = options.args,
data = options.data,
xScale = options.xScale,
width = options.lineWidth,
index = args.index,
x = xScale(args.x),
y = options.yScale(args.y),
x2;
if (data.length - 1 > index) {
x2 = options.xScale(data[index + 1][0]);
context.clearRect(x - width, y - width, x2 - x + 2 * width, 2 * width);
}
};
}
}
});
/** Bars **/
Flotr.addType('bars', {
options: {
show: false, // => setting to true will show bars, false will hide
lineWidth: 2, // => in pixels
barWidth: 1, // => in units of the x axis
fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill
fillColor: null, // => fill color
fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
horizontal: false, // => horizontal bars (x and y inverted)
stacked: false, // => stacked bar charts
centered: true, // => center the bars to their x axis value
topPadding: 0.1, // => top padding in percent
grouped: false // => groups bars together which share x value, hit not supported.
},
stack : {
positive : [],
negative : [],
_positive : [], // Shadow
_negative : [] // Shadow
},
draw : function (options) {
var
context = options.context;
this.current += 1;
context.save();
context.lineJoin = 'miter';
// @TODO linewidth not interpreted the right way.
context.lineWidth = options.lineWidth;
context.strokeStyle = options.color;
if (options.fill) context.fillStyle = options.fillStyle;
this.plot(options);
context.restore();
},
plot : function (options) {
var
data = options.data,
context = options.context,
shadowSize = options.shadowSize,
i, geometry, left, top, width, height;
if (data.length < 1) return;
this.translate(context, options.horizontal);
for (i = 0; i < data.length; i++) {
geometry = this.getBarGeometry(data[i][0], data[i][1], options);
if (geometry === null) continue;
left = geometry.left;
top = geometry.top;
width = geometry.width;
height = geometry.height;
if (options.fill) context.fillRect(left, top, width, height);
if (shadowSize) {
context.save();
context.fillStyle = 'rgba(0,0,0,0.05)';
context.fillRect(left + shadowSize, top + shadowSize, width, height);
context.restore();
}
if (options.lineWidth) {
context.strokeRect(left, top, width, height);
}
}
},
translate : function (context, horizontal) {
if (horizontal) {
context.rotate(-Math.PI / 2);
context.scale(-1, 1);
}
},
getBarGeometry : function (x, y, options) {
var
horizontal = options.horizontal,
barWidth = options.barWidth,
centered = options.centered,
stack = options.stacked ? this.stack : false,
lineWidth = options.lineWidth,
bisection = centered ? barWidth / 2 : 0,
xScale = horizontal ? options.yScale : options.xScale,
yScale = horizontal ? options.xScale : options.yScale,
xValue = horizontal ? y : x,
yValue = horizontal ? x : y,
stackOffset = 0,
stackValue, left, right, top, bottom;
if (options.grouped) {
this.current / this.groups;
xValue = xValue - bisection;
barWidth = barWidth / this.groups;
bisection = barWidth / 2;
xValue = xValue + barWidth * this.current - bisection;
}
// Stacked bars
if (stack) {
stackValue = yValue > 0 ? stack.positive : stack.negative;
stackOffset = stackValue[xValue] || stackOffset;
stackValue[xValue] = stackOffset + yValue;
}
left = xScale(xValue - bisection);
right = xScale(xValue + barWidth - bisection);
top = yScale(yValue + stackOffset);
bottom = yScale(stackOffset);
// TODO for test passing... probably looks better without this
if (bottom < 0) bottom = 0;
// TODO Skipping...
// if (right < xa.min || left > xa.max || top < ya.min || bottom > ya.max) continue;
return (x === null || y === null) ? null : {
x : xValue,
y : yValue,
xScale : xScale,
yScale : yScale,
top : top,
left : Math.min(left, right) - lineWidth / 2,
width : Math.abs(right - left) - lineWidth,
height : bottom - top
};
},
hit : function (options) {
var
data = options.data,
args = options.args,
mouse = args[0],
n = args[1],
x = options.xInverse(mouse.relX),
y = options.yInverse(mouse.relY),
hitGeometry = this.getBarGeometry(x, y, options),
width = hitGeometry.width / 2,
left = hitGeometry.left,
height = hitGeometry.y,
geometry, i;
for (i = data.length; i--;) {
geometry = this.getBarGeometry(data[i][0], data[i][1], options);
if (
// Height:
(
// Positive Bars:
(height > 0 && height < geometry.y) ||
// Negative Bars:
(height < 0 && height > geometry.y)
) &&
// Width:
(Math.abs(left - geometry.left) < width)
) {
n.x = data[i][0];
n.y = data[i][1];
n.index = i;
n.seriesIndex = options.index;
}
}
},
drawHit : function (options) {
// TODO hits for stacked bars; implement using calculateStack option?
var
context = options.context,
args = options.args,
geometry = this.getBarGeometry(args.x, args.y, options),
left = geometry.left,
top = geometry.top,
width = geometry.width,
height = geometry.height;
context.save();
context.strokeStyle = options.color;
context.lineWidth = options.lineWidth;
this.translate(context, options.horizontal);
// Draw highlight
context.beginPath();
context.moveTo(left, top + height);
context.lineTo(left, top);
context.lineTo(left + width, top);
context.lineTo(left + width, top + height);
if (options.fill) {
context.fillStyle = options.fillStyle;
context.fill();
}
context.stroke();
context.closePath();
context.restore();
},
clearHit: function (options) {
var
context = options.context,
args = options.args,
geometry = this.getBarGeometry(args.x, args.y, options),
left = geometry.left,
width = geometry.width,
top = geometry.top,
height = geometry.height,
lineWidth = 2 * options.lineWidth;
context.save();
this.translate(context, options.horizontal);
context.clearRect(
left - lineWidth,
Math.min(top, top + height) - lineWidth,
width + 2 * lineWidth,
Math.abs(height) + 2 * lineWidth
);
context.restore();
},
extendXRange : function (axis, data, options, bars) {
this._extendRange(axis, data, options, bars);
this.groups = (this.groups + 1) || 1;
this.current = 0;
},
extendYRange : function (axis, data, options, bars) {
this._extendRange(axis, data, options, bars);
},
_extendRange: function (axis, data, options, bars) {
var
max = axis.options.max;
if (_.isNumber(max) || _.isString(max)) return;
var
newmin = axis.min,
newmax = axis.max,
horizontal = options.horizontal,
orientation = axis.orientation,
positiveSums = this.positiveSums || {},
negativeSums = this.negativeSums || {},
value, datum, index, j;
// Sides of bars
if ((orientation == 1 && !horizontal) || (orientation == -1 && horizontal)) {
if (options.centered) {
newmax = Math.max(axis.datamax + options.barWidth, newmax);
newmin = Math.min(axis.datamin - options.barWidth, newmin);
}
}
if (options.stacked &&
((orientation == 1 && horizontal) || (orientation == -1 && !horizontal))){
for (j = data.length; j--;) {
value = data[j][(orientation == 1 ? 1 : 0)]+'';
datum = data[j][(orientation == 1 ? 0 : 1)];
// Positive
if (datum > 0) {
positiveSums[value] = (positiveSums[value] || 0) + datum;
newmax = Math.max(newmax, positiveSums[value]);
}
// Negative
else {
negativeSums[value] = (negativeSums[value] || 0) + datum;
newmin = Math.min(newmin, negativeSums[value]);
}
}
}
// End of bars
if ((orientation == 1 && horizontal) || (orientation == -1 && !horizontal)) {
if (options.topPadding && (axis.max === axis.datamax || (options.stacked && this.stackMax !== newmax))) {
newmax += options.topPadding * (newmax - newmin);
}
}
this.stackMin = newmin;
this.stackMax = newmax;
this.negativeSums = negativeSums;
this.positiveSums = positiveSums;
axis.max = newmax;
axis.min = newmin;
}
});
/** Points **/
Flotr.addType('points', {
options: {
show: false, // => setting to true will show points, false will hide
radius: 3, // => point radius (pixels)
lineWidth: 2, // => line width in pixels
fill: true, // => true to fill the points with a color, false for (transparent) no fill
fillColor: '#FFFFFF', // => fill color. Null to use series color.
fillOpacity: 1, // => opacity of color inside the points
hitRadius: null // => override for points hit radius
},
draw : function (options) {
var
context = options.context,
lineWidth = options.lineWidth,
shadowSize = options.shadowSize;
context.save();
if (shadowSize > 0) {
context.lineWidth = shadowSize / 2;
context.strokeStyle = 'rgba(0,0,0,0.1)';
this.plot(options, shadowSize / 2 + context.lineWidth / 2);
context.strokeStyle = 'rgba(0,0,0,0.2)';
this.plot(options, context.lineWidth / 2);
}
context.lineWidth = options.lineWidth;
context.strokeStyle = options.color;
if (options.fill) context.fillStyle = options.fillStyle;
this.plot(options);
context.restore();
},
plot : function (options, offset) {
var
data = options.data,
context = options.context,
xScale = options.xScale,
yScale = options.yScale,
i, x, y;
for (i = data.length - 1; i > -1; --i) {
y = data[i][1];
if (y === null) continue;
x = xScale(data[i][0]);
y = yScale(y);
if (x < 0 || x > options.width || y < 0 || y > options.height) continue;
context.beginPath();
if (offset) {
context.arc(x, y + offset, options.radius, 0, Math.PI, false);
} else {
context.arc(x, y, options.radius, 0, 2 * Math.PI, true);
if (options.fill) context.fill();
}
context.stroke();
context.closePath();
}
}
});
/**
* Selection Handles Plugin
*
*
* Options
* show - True enables the handles plugin.
* drag - Left and Right drag handles
* scroll - Scrolling handle
*/
(function () {
function isLeftClick (e, type) {
return (e.which ? (e.which === 1) : (e.button === 0 || e.button === 1));
}
function boundX(x, graph) {
return Math.min(Math.max(0, x), graph.plotWidth - 1);
}
function boundY(y, graph) {
return Math.min(Math.max(0, y), graph.plotHeight);
}
var
D = Flotr.DOM,
E = Flotr.EventAdapter,
_ = Flotr._;
Flotr.addPlugin('selection', {
options: {
pinchOnly: null, // Only select on pinch
mode: null, // => one of null, 'x', 'y' or 'xy'
color: '#B6D9FF', // => selection box color
fps: 20 // => frames-per-second
},
callbacks: {
'flotr:mouseup' : function (event) {
var
options = this.options.selection,
selection = this.selection,
pointer = this.getEventPosition(event);
if (!options || !options.mode) return;
if (selection.interval) clearInterval(selection.interval);
if (this.multitouches) {
selection.updateSelection();
} else
if (!options.pinchOnly) {
selection.setSelectionPos(selection.selection.second, pointer);
}
selection.clearSelection();
if(selection.selecting && selection.selectionIsSane()){
selection.drawSelection();
selection.fireSelectEvent();
this.ignoreClick = true;
}
},
'flotr:mousedown' : function (event) {
var
options = this.options.selection,
selection = this.selection,
pointer = this.getEventPosition(event);
if (!options || !options.mode) return;
if (!options.mode || (!isLeftClick(event) && _.isUndefined(event.touches))) return;
if (!options.pinchOnly) selection.setSelectionPos(selection.selection.first, pointer);
if (selection.interval) clearInterval(selection.interval);
this.lastMousePos.pageX = null;
selection.selecting = false;
selection.interval = setInterval(
_.bind(selection.updateSelection, this),
1000 / options.fps
);
},
'flotr:destroy' : function (event) {
clearInterval(this.selection.interval);
}
},
// TODO This isn't used. Maybe it belongs in the draw area and fire select event methods?
getArea: function() {
var
s = this.selection.selection,
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;
}
});
})();
(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 () {
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 (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));
}
}
}
}
}
});
})();
(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 () {
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('
');
// Add x axis title
if (a.x.options.title && a.x.used)
html.push(
']+)/.exec(a),d=c.createElement(b&&k[b[1].toLowerCase()]||"div"),e=[];d.innerHTML=a;var f=d.childNodes;d=d.firstChild,e.push(d);while(d=d.nextSibling)d.nodeType==1&&e.push(d);return e}():A(a)?[a.cloneNode(!0)]:[]},N.doc=function(){var a=this.viewport();return{width:Math.max(c.body.scrollWidth,d.scrollWidth,a.width),height:Math.max(c.body.scrollHeight,d.scrollHeight,a.height)}},N.firstChild=function(a){for(var b=a.childNodes,c=0,d=b&&b.length||0,e;c= 0 && newIndex < components.length && this.remove(component)) {
if (this.rendered) {
if (newIndex === components.length)
this.node.appendChild(component.container);
else
this.node.insertBefore(component.container, components[newIndex].container);
}
components.splice(newIndex, 0, component);
this._updateClasses();
}
return this;
},
/**
* Gets the position of a component.
*
* @param {envision.Component} component
*/
indexOf : function (component) {
return _.indexOf(this.components, component);
},
/**
* Gets the component at a position.
*
* @param {envision.Component} component
* @returns {envision.Component} The found component.
*/
getComponent : function (index) {
var components = this.components;
if (index < components.length) return components[index];
},
/**
* Gets whether or not the component is the first component
* in the visualization.
*
* @param {envision.Component} component
* @returns {Boolean}
*/
isFirst : function (component) {
return (this.indexOf(component) === 0 ? true : false);
},
/**
* Gets whether or not the component is the last component
* in the visualization.
*
* @param {envision.Component} component
* @returns {Boolean}
*/
isLast : function (component) {
return (this.indexOf(component) === this.components.length - 1 ? true : false);
},
/**
* Destroys the visualization.
*
* This empties the container and destroys all the components which are part
* of the visualization.
*/
destroy : function () {
_.each(this.components, function (component) {
component.destroy();
});
bonzo(this.container).empty();
},
_renderComponent : function (component) {
var
container = bonzo.create(T_COMPONENT_CONTAINER)[0];
bonzo(this.node).append(container);
component.render(container);
},
_updateClasses : function () {
var
components = this.components,
first = 0,
last = components.length -1,
node;
_.each(components, function (component, index) {
node = bonzo(component.container);
if (index === first)
node.addClass(CN_FIRST);
else
node.removeClass(CN_FIRST);
if (index === last)
node.addClass(CN_LAST);
else
node.removeClass(CN_LAST);
});
}
};
envision.Visualization = Visualization;
})();
// Component Class
(function () {
var
V = envision,
CN_COMPONENT = 'envision-component',
T_COMPONENT = '';
/**
* @summary Defines a visualization component.
*
* @description Components are the building blocks of a visualization,
* representing one typically graphical piece of the vis. This class manages
* the options, DOM and API construction for an adapter which handles the
* actual drawing of the visualization piece.
*
* Adapters can take the form of an actual object, a constructor function
* or a function returning an object. Only one of these will be used. If
* none is submitted, the default adapter Flotr2 is used.
*
* @param {String} [name] A name for the component.
* @param {Element} [element] A container element for the component.
* @param {Number} [height] An explicit component height.
* @param {Number} [width] An explicit component width.
* @param {Array} [data] An array of data. Data may be formatted for
* envision or for the adapter itself, in which case skipPreprocess will
* also need to be submitted.
* @param {Boolean} [skipPreprocess] Skip data preprocessing. This is useful
* when using the native data format for an adapter.
* @param {Object} [adapter] An adapter object.
* @param {Function} [adapterConstructor] An adapter constructor to be
* instantiated by the component.
* @param {Function} [adapterCallback] An callback invoked by the component
* returning an adapter.
* @param {Object} [config] Configuration for the adapter.
*
* @memberof envision
* @class
*/
function Component (options) {
options = options || {};
var
node = bonzo.create(T_COMPONENT)[0];
this.options = options;
this.node = node;
// Instantiate Adapter
if (options.adapter) {
this.api = options.adapter;
} else if (options.adapterConstructor) {
this.api = new options.adapterConstructor(options.config);
} else if (options.adapterCallback) {
this.api = options.adapterCallback.call(null, options.config);
} else if (options.config) {
this.api = new V.adapters.flotr.Child(options.config || {});
}
// this.id = _.uniqueId(CN_COMPONENT);
this.preprocessors = [];
}
Component.prototype = {
/**
* Render the component.
*
* If no element is submitted, the component will
* render in the element configured in the constructor.
*
* @param {Element} [element]
*/
render : function (element) {
var
node = this.node,
options = this.options;
element = element || options.element;
if (!element) throw 'No element to render within.';
bonzo(element)
.addClass(options.name || '')
.append(this.node);
this._setDimension('width');
this._setDimension('height');
this.container = element;
this.draw(options.data, options.config);
},
/**
* Draw the component.
*
* @param {Array} [data] Data for the adapter.
* @param {Object} [options] Configuration object for the adapters draw method.
*/
draw : function (data, config) {
var
api = this.api,
options = this.options,
preprocessors = this.preprocessors,
clientData;
clientData = data = data || options.data;
config = config || options.config;
if (!options.skipPreprocess && data) {
clientData = [];
_.each(api.getDataArray(data), function (d, index) {
var
preprocessor = preprocessors[index] || new V.Preprocessor(),
isArray = _.isArray(d),
isFunction = _.isFunction(d),
unprocessed = isArray ? d : (isFunction ? d : d.data),
processData = options.processData,
range = api.range(config),
min = range.min,
max = range.max,
resolution = this.node.clientWidth,
dataArray = d,
processed, objectData;
// For object data
if (!isFunction && !isArray) {
dataArray = d.data;
objectData = _.extend({}, d);
}
// Do data function preprocessing
if (isFunction) {
processed = data(min, max, resolution);
} else {
// Update if new data
if (dataArray !== preprocessor.data) {
preprocessor.setData(dataArray);
} else {
preprocessor.reset();
}
// Do custom callback preprocessing
if (processData) {
processData.apply(this, [{
preprocessor : preprocessor,
min : min,
max : max,
resolution : resolution
}]);
processed = preprocessor.getData();
}
// Default preprocessing
else {
processed = preprocessor
.bound(min, max)
.subsampleMinMax(resolution)
.getData();
}
}
// If present, transform the data for the API
if (api.transformData) {
processed = api.transformData(processed);
}
// Object Data
if (objectData) {
objectData.data = processed;
clientData.push(objectData);
}
// Array Data
else {
clientData.push(processed);
}
}, this);
}
if (api) api.draw(clientData, config, this.node);
},
/**
* Trigger an event on the component's API.
*
* Arguments are passed through to the API.
*/
trigger : function () {
this.api.trigger.apply(this.api, Array.prototype.concat.apply([this], arguments));
},
/**
* Attach to an event on the component's API.
*
* Arguments are passed through to the API.
*/
attach : function () {
this.api.attach.apply(this.api, Array.prototype.concat.apply([this], arguments));
},
/**
* Detach a listener from an event on the component's API.
*
* Arguments are passed through to the API.
*/
detach : function () {
this.api.detach.apply(this.api, Array.prototype.concat.apply([this], arguments));
},
/**
* Destroy the component.
*
* Empties the container and calls the destroy method on the
* component's API.
*/
destroy : function () {
if (this.api && this.api.destroy) this.api.destroy();
bonzo(this.container).empty();
},
_setDimension : function (attribute) {
var
node = this.node,
options = this.options;
if (options[attribute]) {
bonzo(node).css(attribute, options[attribute]);
} else {
//options[attribute] = parseInt(bonzo(node).css(attribute), 10);
options[attribute] = node.clientWidth;
}
this[attribute] = options[attribute];
}
};
V.Component = Component;
})();
// Interaction Class
(function () {
var H = envision;
/**
* @summary Defines an interaction between components.
*
* @description This class defines interactions in which actions are triggered
* by leader components and reacted to by follower components. These actions
* are defined as configurable mappings of trigger events and event consumers.
* It is up to the adapter to implement the triggers and consumers.
*
* A component may be both a leader and a follower. A leader which is a
* follower will react to actions triggered by other leaders, but will safely
* not react to its own. This allows for groups of components to perform a
* common action.
*
* Optionally, actions may be supplied with a callback executed before the
* action is consumed. This allows for quick custom functionality to be added
* and is how advanced data management (ie. live Ajax data) may be implemented.
*
* This class follow an observer mediator pattern.
*
* @param {envision.Component|Array} [leader] Component(s) to lead the
* interaction
*
* @memberof envision
* @class
*/
function Interaction(options) {
this.options = options = options || {};
this.actions = [];
this.actionOptions = [];
this.followers = [];
this.leaders = [];
this.prevent = {};
if (options.leader) {
this.leader(options.leader);
}
}
Interaction.prototype = {
/**
* Add a component as an interaction leader.
*
* @param {envision.Component} component
*/
leader : function (component) {
this.leaders.push(component);
_.each(this.actions, function (action, i) {
this._bindLeader(component, action, this.actionOptions[i]);
}, this);
return this;
},
/**
* Add a component as an interaction leader.
*
* @param {envision.Component} component
*/
follower : function (component) {
this.followers.push(component);
return this;
},
/**
* Adds an array of components as both followers and leaders.
*
* @param {Array} components An array of components
*/
group : function (components) {
if (!_.isArray(components)) components = [components];
_.each(components, function (component) {
this.leader(component);
this.follower(component);
}, this);
return this;
},
/**
* Adds an action to the interaction.
*
* The action may be optionally configured with the options argument.
* Currently the accepts a callback member, invoked after an action
* is triggered and before it is consumed by followers.
*
* @param {Object} action
* @param {Object} [options]
*/
add : function (action, options) {
this.actions.push(action);
this.actionOptions.push(options);
_.each(this.leaders, function (leader) {
this._bindLeader(leader, action, options);
}, this);
return this;
},
_bindLeader : function (leader, action, options) {
_.each(action.events, function (e) {
var
handler = e.handler || e,
consumer = e.consumer || e;
leader.attach(handler, _.bind(function (leader, result) {
if (this.prevent[name]) return;
// Apply custom callback configured for this action
if (options && options.callback) {
options.callback.call(this, result);
}
this.prevent[name] = true; // Prevent recursions for this name
try {
_.each(this.followers, function (follower) {
if (leader === follower) return; // Skip leader (recursion)
follower.trigger(consumer, result);
}, this);
} catch (e) {
this.prevent[name] = false;
throw e;
}
this.prevent[name] = false;
}, this));
}, this);
}
};
H.Interaction = Interaction;
})();
// Preprocessor Class
(function () {
/**
* @summary Data preprocessor.
*
* @description Data can be preprocessed before it is rendered by an adapter.
*
* This has several important performance considerations. If data will be
* rendered repeatedly or on slower browsers, it will be faster after being
* optimized.
*
* First, data outside the boundaries does not need to be rendered. Second,
* the resolution of the data only needs to be at most the number of pixels
* in the width of the visualization.
*
* Performing these optimizations will limit memory overhead, important
* for garbage collection and performance on old browsers, as well as drawing
* overhead, important for mobile devices, old browsers and large data sets.
*
* @param {Array} [data] The data for processing.
*
* @memberof envision
* @class
*/
function Preprocessor (options) {
options = options || {};
/**
* Returns data.
*/
this.getData = function () {
if (this.bounded) bound(this);
return this.processing;
};
this.reset = function () {
this.processing = this.data;
return this;
};
/**
* Set the data object.
*/
this.setData = function (data) {
var
i, length;
if (!_.isArray(data)) throw new Error('Array expected.');
if (data.length < 2) throw new Error('Data must contain at least two dimensions.');
length = data[0].length;
for (i = data.length; i--;) {
if (!_.isArray(data[i])) throw new Error('Data dimensions must be arrays.');
if (data[i].length !== length) throw new Error('Data dimensions must contain the same number of points.');
}
this.processing = data;
this.data = data;
return this;
};
if (options.data) this.setData(options.data);
}
function getStartIndex (data, min) {
var
i = _.sortedIndex(data, min);
// Include point outside range when not exact match
if (data[i] > min && i > 0) i--;
return i;
}
function getEndIndex (data, max) {
return _.sortedIndex(data, max);
}
function bound (that) {
delete that.bounded;
var
data = that.processing,
length = that.length(),
x = data[0],
y = data[1],
min = that.min || 0,
max = that.max || length,
start = getStartIndex(x, min),
end = getEndIndex(x, max);
that.processing = [
x.slice(start, end + 1),
y.slice(start, end + 1)
];
that.start = start;
that.end = end;
}
Preprocessor.prototype = {
/**
* Returns the length of the data set.
*
* @return {Number} Length of the data set.
*/
length : function () {
return this.getData()[0].length;
},
/**
* Bounds the data set at within a range.
*
* @param {Number} min
* @param {Number} max
*/
bound : function (min, max) {
if (!_.isNumber(min) || !_.isNumber(max)) return this;
this.min = min;
this.max = max;
this.bounded = true;
return this;
},
/**
* Subsample data using MinMax.
*
* MinMax will display the extrema of the subsample intervals. This is
* slower than regular interval subsampling but necessary for data that
* is very non-homogenous.
*
* @param {Number} resolution
*/
subsampleMinMax : function (resolution) {
var bounded = this.bounded;
delete this.bounded;
var
data = this.processing,
length = this.length(),
x = data[0],
y = data[1],
start = bounded ? getStartIndex(x, this.min) : 0,
end = bounded ? getEndIndex(x, this.max) : length - 1,
count = (resolution - 2) / 2,
newX = [],
newY = [],
min = Number.MAX_VALUE,
max = -Number.MAX_VALUE,
minI = 1,
maxI = 1,
unit = (end - start)/ count,
position, datum, i, j;
if (end - start + 1 > resolution) {
newX.push(x[start]);
newY.push(y[start]);
position = start + unit;
for (i = start; i < end; i++) {
if (i === Math.round(position)) {
position += unit;
j = Math.min(maxI, minI);
newX.push(x[j]);
newY.push(y[j]);
j = Math.max(maxI, minI);
newX.push(x[j]);
newY.push(y[j]);
minI = i;
min = y[minI];
maxI = i;
max = y[maxI];
} else {
if (y[i] > max) {
max = y[i];
maxI = i;
}
if (y[i] < min) {
min = y[i];
minI = i;
}
}
}
if (i < position) {
newX.push(x[minI]);
newY.push(min);
newX.push(x[maxI]);
newY.push(max);
}
// Last
newX.push(x[end]);
newY.push(y[end]);
this.processing = [newX, newY];
this.start = start;
this.end = end;
} else {
this.bounded = bounded;
}
return this;
},
/**
* Subsample data at a regular interval for resolution.
*
* This is the fastest subsampling and good for monotonic data and fairly
* homogenous data (not a lot of up and down).
*
* @param {Number} resolution
*/
subsample : function (resolution) {
var bounded = this.bounded;
delete this.bounded;
var
data = this.processing,
length = this.length(),
x = data[0],
y = data[1],
start = bounded ? getStartIndex(x, this.min) : 0,
end = bounded ? getEndIndex(x, this.max) : length - 1,
unit = (end - start + 1) / resolution,
newX = [],
newY = [],
i, index;
if (end - start + 1 > resolution) {
// First
newX.push(x[start]);
newY.push(y[start]);
for (i = 1; i < resolution; i++) {
if (i * unit >= end - unit) break;
index = Math.round(i * unit) + start;
newX.push(x[index]);
newY.push(y[index]);
}
// Last
newX.push(x[end]);
newY.push(y[end]);
this.processing = [newX, newY];
this.start = start;
this.end = end;
} else {
this.bounded = bounded;
}
return this;
},
interpolate : function (resolution) {
var bounded = this.bounded;
delete this.bounded;
var
data = this.processing,
length = this.length(),
x = data[0],
y = data[1],
start = bounded ? getStartIndex(x, this.min) : 0,
end = bounded ? getEndIndex(x, this.max) : length - 1,
unit = (x[end] - x[start]) / resolution,
newX = [],
newY = [],
i, j, delta;
newX.push(x[start]);
newY.push(y[start]);
if (end - start + 1 < resolution) {
for (i = start; i < end; i++) {
delta = x[i + 1] - x[i];
newX.push(x[i]);
newY.push(y[i]);
for (j = x[i + 0] + unit; j < x[i + 1]; j += unit) {
newX.push(j);
newY.push(cubicHermiteSpline(
j,
x[i - 1],
y[i - 1],
x[i + 0],
y[i + 0],
x[i + 1],
y[i + 1],
x[i + 2],
y[i + 2]
));
}
}
this.processing = [newX, newY];
this.start = start;
this.end = end;
}
return this;
}
};
function cubicHermiteSpline (x, tk0, pk0, tk1, pk1, tk2, pk2, tk3, pk3) {
var
t = (x - tk1) / (tk2 - tk1),
t1 = 1 - t,
h00 = (1 + 2 * t) * t1 * t1,
h10 = t * t1 * t1,
h01 = t * t * (3 - 2 * t),
h11 = t * t * (t - 1),
mk = (pk2 - pk1) / (2 * (tk2 - tk1)) + (typeof pk0 === 'undefined' ? 0 : (pk1 - pk0) / (2 * (tk1 - tk0))),
mk1 = (typeof pk3 === 'undefined' ? 0 : (pk3 - pk2) / (2 * (tk3 - tk2))) + (pk2 - pk1) / (2 * (tk2 - tk1)),
px = h00 * pk1 + h10 * (tk2 - tk1) * mk + h01 * pk2 + h11 * (tk2 - tk1) * mk1;
return px;
}
envision.Preprocessor = Preprocessor;
}());
/**
* Actions namespace. Actions are configurations for
* common use cases when building Interactions.
*/
envision.actions = envision.actions || {};
envision.actions.hit = {
events : [
'hit',
'mouseout'
]
};
envision.actions.selection = {
events : [
{
handler : 'select',
consumer : 'zoom'
},
// Reset on click, avoids re-drawing the leader.
{
handler : 'click',
consumer : 'reset'
}
]
};
envision.actions.zoom = {
events : [
// Zoom on the followers as selecting
{
handler : 'select',
consumer : 'zoom'
},
// Zoom on the leader after mouseup
'zoom',
// Reset all on click
'reset'
]
};
/**
* Adapters namespace. These are component adapters for external
* librares. Envision.js ships with a Flotr2 adapter.
*/
envision.adapters = envision.adapters || {};
envision.adapters.flotr = {};
/*
* Flotr Default Options
*/
envision.adapters.defaultOptions = {
grid : {
outlineWidth : 0,
labelMargin : 0,
horizontalLines : false,
verticalLines : false
},
bars : {
show : false,
barWidth : 0.5,
fill : true,
lineWidth : 1,
fillOpacity : 1
},
lines : {
lineWidth : 1
},
xaxis : {
margin : false,
tickDecimals: 0,
showLabels : false
},
yaxis : {
margin : false,
showLabels : false
},
shadowSize : false
};
/**
* Flotr Adapter
*/
(function () {
var
V = envision,
A = envision.adapters,
E = Flotr.EventAdapter,
DEFAULTS = A.defaultOptions;
function Child (options) {
this.options = options || {};
this.flotr = null;
this._flotrDefaultOptions();
}
Child.prototype = {
destroy : function () {
this.flotr.destroy();
},
draw : function (data, flotr, node) {
var
options = this.options;
data = this.getDataArray(data || options.data);
if (flotr) {
flotr = Flotr.merge(flotr, Flotr.clone(options));
} else {
flotr = options;
}
options.data = data;
if (!flotr) throw 'No graph submitted.';
this.flotr = Flotr.draw(node, data, flotr);
},
range : function (flotr) {
var
axis = flotr.xaxis;
return {
min : axis.min,
max : axis.max
};
},
// Transform for Flotr
transformData : function (data) {
var
length = data[0].length,
dimension = data.length,
transformed = [],
point,
i, j;
for (i = 0; i < length; i++) {
point = [];
for (j = 0; j < dimension; j++) {
point.push(data[j][i]);
}
transformed.push(point);
}
return transformed;
},
getDataArray : function (data) {
if (
data[0] && // Data exists and
(
!_.isArray(data[0]) || // data not an array
!data[0].length || // data is an empty series
(data[0][0] && _.isArray(data[0][0])) // data is a series
)
)
return data;
else
return [data];
},
_flotrDefaultOptions : function (options) {
var o = options || this.options,
i;
for (i in DEFAULTS) {
if (DEFAULTS.hasOwnProperty(i)) {
if (_.isUndefined(o[i])) {
o[i] = DEFAULTS[i];
} else {
_.defaults(o[i], DEFAULTS[i]);
}
}
}
},
attach : function (component, name, callback) {
var
event = this.events[name] || {},
handler = event.handler || false;
name = event.name || false;
if (handler) {
return E.observe(component.node, name, function () {
var
args = [component].concat(Array.prototype.slice.call(arguments)),
result = handler.apply(this, args);
return callback.apply(null, [component, result]);
});
} else {
return false;
}
},
detach : function (component, name, callback) {
return E.stopObserve(component.node, name, handler);
},
trigger : function (component, name, options) {
var
event = this.events[name],
consumer = event.consumer || false;
return consumer ? consumer.apply(this, [component, options]) : false;
},
events : {
hit : {
name : 'flotr:hit',
handler : function (component, hit) {
var
x = hit.x,
y = hit.y,
graph = component.api.flotr,
options;
// Normalized hit:
options = {
data : {
index : hit.index,
x : x,
y : y
},
x : graph.axes.x.d2p(x),
y : graph.axes.y.d2p(y)
};
return options;
},
consumer : function (component, hit) {
var
graph = component.api.flotr,
o;
// TODO this is a hack;
// the hit plugin should expose an API to do this easily
o = {
x : hit.data.x,
y : hit.data.y || 1,
relX : hit.x,
relY : hit.y || 1
};
graph.hit.hit(o);
}
},
select : {
name : 'flotr:selecting',
handler : selectHandler,
consumer : function (component, selection) {
var
graph = component.api.flotr,
axes = graph.axes,
data = selection.data || {},
options = {},
x = selection.x,
y = selection.y;
if (!x && data.x) {
// Translate data to pixels
x = data.x;
options.x1 = x.min;
options.x2 = x.max;
} else if (x) {
// Use pixels
options.x1 = axes.x.p2d(x.min);
options.x2 = axes.x.p2d(x.max);
}
if (!y && data.y) {
// Translate data to pixels
y = data.y;
options.y1 = y.min;
options.y2 = y.max;
} else if (y) {
// Use pixels
options.y1 = axes.y.d2p(y.min);
options.y2 = axes.y.d2p(y.max);
}
graph.selection.setSelection(options);
}
},
zoom : {
name : 'flotr:select',
handler : function (component, selection) {
var options = selectHandler(component, selection);
component.trigger('zoom', options);
return options;
},
consumer : function (component, selection) {
var
x = selection.data.x,
y = selection.data.y,
options = {};
if (x) {
options.xaxis = {
min : x.min,
max : x.max
};
}
if (y) {
options.yaxis = {
min : y.min,
max : y.max
};
}
component.draw(null, options);
}
},
mouseout : {
name : 'flotr:mouseout',
handler : function (component) {
},
consumer : function (component) {
component.api.flotr.hit.clearHit();
}
},
reset : {
name : 'flotr:click',
handler : function (component) {
component.draw();
},
consumer : function (component) {
component.draw();
}
},
click : {
name : 'flotr:click',
handler : function (component) {
var
min = component.api.flotr.axes.x.min,
max = component.api.flotr.axes.x.max;
return {
data : {
x : {
min : min,
max : max
}
},
x : {
min : component.api.flotr.axes.x.d2p(min),
max : component.api.flotr.axes.x.d2p(max)
}
};
},
consumer : function (component, selection) {
var
x = selection.data.x,
y = selection.data.y,
options = {};
if (x) {
options.xaxis = {
min : x.min,
max : x.max
};
}
if (y) {
options.yaxis = {
min : y.min,
max : y.max
};
}
component.draw(null, options);
}
}
}
};
function selectHandler (component, selection) {
var
mode = component.options.config.selection.mode,
axes = component.api.flotr.axes,
datax, datay, x, y, options;
if (mode.indexOf('x') !== -1) {
datax = {};
datax.min = selection.x1;
datax.max = selection.x2;
x = {};
x.min = axes.x.d2p(selection.x1);
x.max = axes.x.d2p(selection.x2);
}
if (mode.indexOf('y') !== -1) {
datay = {};
datay.min = selection.y1;
datay.max = selection.y2;
y = {};
y.min = axes.y.d2p(selection.y1);
y.max = axes.y.d2p(selection.y2);
}
// Normalized selection:
options = {
data : {
x : datax,
y : datay
},
x : x,
y : y
};
return options;
}
A.flotr.Child = Child;
})();
/** Lines **/
Flotr.addType('lite-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
},
/**
* 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.lineCap = 'butt';
context.lineWidth = lineWidth;
context.strokeStyle = options.color;
this.plot(options);
context.restore();
},
plot : function (options) {
var
context = options.context,
xScale = options.xScale,
yScale = options.yScale,
data = options.data,
length = data.length - 1,
zero = yScale(0),
x0, y0;
if (length < 1) return;
x0 = xScale(data[0][0]);
y0 = yScale(data[0][1]);
context.beginPath();
context.moveTo(x0, y0);
for (i = 0; i < length; ++i) {
context.lineTo(
xScale(data[i+1][0]),
yScale(data[i+1][1])
);
}
if (!options.fill || options.fill && !options.fillBorder) context.stroke();
if (options.fill){
x0 = xScale(data[0][0]);
context.fillStyle = options.fillStyle;
context.lineTo(xScale(data[length][0]), zero);
context.lineTo(x0, zero);
context.lineTo(x0, yScale(data[0][1]));
context.fill();
if (options.fillBorder) {
context.stroke();
}
}
},
extendYRange : function (axis, data, options, lines) {
var o = axis.options;
// HACK
if ((!o.max && o.max !== 0) || (!o.min && o.min !== 0)) {
axis.max += options.lineWidth * 0.01;
axis.min -= options.lineWidth * 0.01;
/*
axis.max = axis.p2d((axis.d2p(axis.max) + options.lineWidth));
axis.min = axis.p2d((axis.d2p(axis.max) - options.lineWidth));
*/
}
}
});
/** Bars **/
Flotr.addType('whiskers', {
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
},
stack : {
positive : [],
negative : [],
_positive : [], // Shadow
_negative : [] // Shadow
},
draw : function (options) {
var
context = options.context;
context.save();
context.lineJoin = 'miter';
context.lineCap = 'butt';
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,
xScale = options.xScale,
yScale = options.yScale,
zero = yScale(0),
i, x;
if (data.length < 1) return;
context.translate(-options.lineWidth, 0);
context.beginPath();
for (i = 0; i < data.length; i++) {
x = xScale(data[i][0]);
context.moveTo(x, zero);
context.lineTo(x, yScale(data[i][1]));
}
context.closePath();
context.stroke();
},
drawHit : function (options) {
var
args = options.args,
context = options.context,
shadowSize = options.shadowSize,
xScale = options.xScale,
yScale = options.yScale,
zero = yScale(0),
x = xScale(args.x),
y = yScale(args.y);
context.save();
context.translate(-options.lineWidth, 0);
context.beginPath();
context.moveTo(x, zero);
context.lineTo(x, y);
context.closePath();
context.stroke();
context.restore();
},
clearHit: function (options) {
var
args = options.args,
context = options.context,
shadowSize = options.shadowSize,
xScale = options.xScale,
yScale = options.yScale,
lineWidth = options.lineWidth,
zero = yScale(0),
x = xScale(args.x),
y = yScale(args.y);
context.save();
context.clearRect(x - 2 * lineWidth, y - lineWidth, 4 * lineWidth, zero - y + lineWidth);
context.restore();
}
});
/**
* Components namespace. These are standalone, custom components
* APIs for widgets, decorations, flair.
*/
envision.components = envision.components || {};
(function () {
function QuadraticDrawing (options) {
this.options = options || {};
}
QuadraticDrawing.prototype = {
height : null,
width : null,
rendered : false,
render : function (node) {
var
canvas = document.createElement('canvas'),
offset = bonzo(node).offset();
this.height = offset.height;
this.width = offset.width;
bonzo(canvas)
.attr('height', offset.height)
.attr('width', offset.width)
.css({
position : 'absolute',
top : '0px',
left : '0px'
});
node.appendChild(canvas);
bonzo(node).css({
position : 'relative'
});
if (typeof FlashCanvas !== 'undefined') FlashCanvas.initElement(canvas);
this.context = canvas.getContext('2d');
this.rendered = true;
},
draw : function (data, options, node) {
if (!this.rendered) this.render(node);
var
context = this.context,
height = this.height,
width = this.width,
half = Math.round(height / 2) - 0.5,
min, max;
options = options || { min : width / 2, max : width / 2};
min = options.min + 0.5;
max = options.max + 0.5;
context.clearRect(0, 0, width, height);
if (min || max) {
context.save();
context.strokeStyle = this.options.strokeStyle || '#B6D9FF';
context.fillStyle = this.options.fillStyle || 'rgba(182, 217, 255, .4)';
context.beginPath();
// Left
if (min <= 1) {
context.moveTo(0, height);
context.lineTo(0, -0.5);
} else {
context.moveTo(min, height);
context.quadraticCurveTo(min, half, Math.max(min - half, min / 2), half);
context.lineTo(Math.min(half, min / 2), half);
context.quadraticCurveTo(0, half, 0.5, -0.5);
}
// Top
context.lineTo(width - 0.5, -0.5);
// Right
if (max >= width - 1) {
context.lineTo(max, height);
} else {
context.quadraticCurveTo(width, half, Math.max(width - half, width - (width - max) / 2), half);
context.lineTo(Math.min(max + half, width - (width - max) / 2), half);
context.quadraticCurveTo(max, half, max, height);
}
context.stroke();
context.closePath();
context.fill();
context.restore();
}
},
trigger : function (component, name, options) {
if (name === 'zoom') {
this.zoom(component, options);
} else if (name === 'reset') {
this.reset(component);
}
},
zoom : function (component, options) {
var
x = options.x || {},
min = x.min,
max = x.max,
api = component.api;
component.draw(null, {
min : min,
max : max
});
},
reset : function (component) {
component.draw(null, {
min : component.width / 2,
max : component.width / 2
});
}
};
envision.components.QuadraticDrawing = QuadraticDrawing;
})();
/**
* Templates namespace.
*
* Templates are pre-built interactive visualizations fitting common
* use-cases. These include several components together with
* interactions and configuration for each. They may have their own
* custom configuration options as well.
*/
envision.templates = envision.templates || {};
(function () {
var
V = envision;
// Custom data processor
function processData (options) {
var
resolution = options.resolution;
options.preprocessor
.bound(options.min, options.max)
.subsampleMinMax(resolution + Math.round(resolution / 3));
}
function getDefaults () {
return {
price : {
name : 'envision-finance-price',
config : {
'lite-lines' : {
lineWidth : 1,
show : true,
fill : true,
fillOpacity : 0.2
},
mouse : {
track: true,
trackY: false,
trackAll: true,
sensibility: 1,
trackDecimals: 4,
position: 'ne'
},
yaxis : {
autoscale : true,
autoscaleMargin : 0.05,
noTicks : 4,
showLabels : true,
min : 0
}
},
processData : processData
},
volume : {
name : 'envision-finance-volume',
config : {
whiskers : {
show : true,
lineWidth : 2
},
mouse: {
track: true,
trackY: false,
trackAll: true
},
yaxis : {
autoscale : true,
autoscaleMargin : 0.5
}
},
processData : processData
},
summary : {
name : 'envision-finance-summary',
config : {
'lite-lines' : {
show : true,
lineWidth : 1,
fill : true,
fillOpacity : 0.2,
fillBorder : true
},
xaxis : {
noTicks: 5,
showLabels : true
},
yaxis : {
autoscale : true,
autoscaleMargin : 0.1
},
handles : {
show : true
},
selection : {
mode : 'x'
},
grid : {
verticalLines : false
}
}
},
connection : {
name : 'envision-finance-connection',
adapterConstructor : V.components.QuadraticDrawing
}
};
}
function Finance (options) {
var
data = options.data,
defaults = getDefaults(),
vis = new V.Visualization({name : 'envision-finance'}),
selection = new V.Interaction(),
hit = new V.Interaction(),
price, volume, connection, summary;
if (options.defaults) {
defaults = Flotr.merge(options.defaults, defaults);
}
defaults.price.data = data.price;
defaults.volume.data = data.volume;
defaults.summary.data = data.summary;
defaults.price.config.mouse.trackFormatter = options.trackFormatter || function (o) {
var
index = o.index,
value;
if (price.api.preprocessor) {
index += price.api.preprocessor.start;
}
value = 'Price: $' + data.price[1][index] + ", Vol: " + data.volume[1][index];
return value;
};
if (options.xTickFormatter) {
defaults.summary.config.xaxis.tickFormatter = options.xTickFormatter;
}
defaults.price.config.yaxis.tickFormatter = options.yTickFormatter || function (n) {
return '$' + n;
};
price = new V.Component(defaults.price);
volume = new V.Component(defaults.volume);
connection = new V.Component(defaults.connection);
summary = new V.Component(defaults.summary);
// Render visualization
vis
.add(price)
.add(volume)
.add(connection)
.add(summary)
.render(options.container);
// Define the selection zooming interaction
selection
.follower(price)
.follower(volume)
.follower(connection)
.leader(summary)
.add(V.actions.selection, options.selectionCallback ? { callback : options.selectionCallback } : null);
// Define the mouseover hit interaction
hit
.group([price, volume])
.add(V.actions.hit);
// Optional initial selection
if (options.selection) {
summary.trigger('select', options.selection);
}
// Members
this.vis = vis;
this.selection = selection;
this.hit = hit;
this.price = price;
this.volume = volume;
this.summary = summary;
}
V.templates.Finance = Finance;
})();
(function () {
var
V = envision;
function getDefaults () {
return {
detail : {
name : 'envision-timeseries-detail',
config : {
'lite-lines' : {
lineWidth : 1,
show : true
}
}
},
summary : {
name : 'envision-timeseries-summary',
config : {
'lite-lines' : {
lineWidth : 1,
show : true
},
handles : {
show : true
},
selection : {
mode : 'x'
},
yaxis : {
autoscale : true,
autoscaleMargin : 0.1
}
}
},
connection : {
name : 'envision-timeseries-connection',
adapterConstructor : V.components.QuadraticDrawing
}
};
}
function TimeSeries (options) {
var
data = options.data,
defaults = getDefaults(),
vis = new V.Visualization({name : 'envision-timeseries'}),
selection = new V.Interaction(),
detail, summary, connection;
// Fill Defaults
if (options.defaults) {
defaults = Flotr.merge(options.defaults, defaults);
}
defaults.detail.data = data.detail;
defaults.summary.data = data.summary;
// Build Components
detail = new V.Component(defaults.detail);
connection = new V.Component(defaults.connection);
summary = new V.Component(defaults.summary);
// Render visualization
vis
.add(detail)
.add(connection)
.add(summary)
.render(options.container);
// Selection action
selection
.follower(detail)
.follower(connection)
.leader(summary)
.add(V.actions.selection, options.selectionCallback ? { callback : options.selectionCallback } : null);
// Optional initial selection
if (options.selection) {
summary.trigger('select', options.selection);
}
this.vis = vis;
this.selection = selection;
this.detail = detail;
this.summary = summary;
}
V.templates.TimeSeries = TimeSeries;
})();
(function () {
var
V = envision,
Zoom;
function defaultsZoom () {
return {
name : 'zoom'
};
}
function defaultsSummary () {
return {
name : 'summary',
config : {
handles : { show : true },
selection : { mode : 'x'}
}
};
}
function getDefaults (options, defaults) {
var o = _.defaults(options, defaults);
o.flotr = _.defaults(o.flotr, defaults.flotr);
return o;
}
Zoom = function (options) {
var
vis = new V.Visualization(),
zoom = new V.Component(getDefaults(options.zoom || {}, defaultsZoom())),
summary = new V.Component(getDefaults(options.summary || {}, defaultsSummary())),
interaction = new V.Interaction({leader : summary});
vis
.add(zoom)
.add(summary);
interaction.add(V.actions.selection);
interaction.follower(zoom);
this.vis = vis;
this.interaction = interaction;
if (options.container) {
this.render(options.container);
}
};
Zoom.prototype = {
render : function (container) {
this.vis.render(container);
}
};
V.templates.Zoom = Zoom;
})();
;
', 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);
}
}
});
})();
/**
* Selection Handles Plugin
*
* Depends upon options.selection.mode
*
* Options
* show - True enables the handles plugin.
* drag - Left and Right drag handles
* scroll - Scrolling handle
*/
(function () {
var D = Flotr.DOM;
Flotr.addPlugin('handles', {
options: {
show: false,
drag: true,
scroll: true
},
callbacks: {
'flotr:afterinit': init,
'flotr:select': handleSelect,
'flotr:mousedown': reset,
'flotr:mousemove': mouseMoveHandler
}
});
function init() {
var
options = this.options,
handles = this.handles,
el = this.el,
scroll, left, right, container;
if (!options.selection.mode || !options.handles.show || 'ontouchstart' in el) return;
handles.initialized = true;
container = D.node('');
options = options.handles;
// Drag handles
if (options.drag) {
right = D.node('');
left = D.node('');
D.insert(container, right);
D.insert(container, left);
D.hide(left);
D.hide(right);
handles.left = left;
handles.right = right;
this.observe(left, 'mousedown', function () {
handles.moveHandler = leftMoveHandler;
});
this.observe(right, 'mousedown', function () {
handles.moveHandler = rightMoveHandler;
});
}
// Scroll handle
if (options.scroll) {
scroll = D.node('');
D.insert(container, scroll);
D.hide(scroll);
handles.scroll = scroll;
this.observe(scroll, 'mousedown', function () {
handles.moveHandler = scrollMoveHandler;
});
}
this.observe(document, 'mouseup', function() {
handles.moveHandler = null;
});
D.insert(el, container);
}
function handleSelect(selection) {
if (!this.handles.initialized) return;
var
handles = this.handles,
options = this.options.handles,
left = handles.left,
right = handles.right,
scroll = handles.scroll;
if (options) {
if (options.drag) {
positionDrag(this, left, selection.x1);
positionDrag(this, right, selection.x2);
}
if (options.scroll) {
positionScroll(
this,
scroll,
selection.x1,
selection.x2
);
}
}
}
function positionDrag(graph, handle, x) {
D.show(handle);
var size = D.size(handle),
l = Math.round(graph.axes.x.d2p(x) - size.width / 2),
t = (graph.plotHeight - size.height) / 2;
D.setStyles(handle, {
'left' : l+'px',
'top' : t+'px'
});
}
function positionScroll(graph, handle, x1, x2) {
D.show(handle);
var size = D.size(handle),
l = Math.round(graph.axes.x.d2p(x1)),
t = (graph.plotHeight) - size.height / 2,
w = (graph.axes.x.d2p(x2) - graph.axes.x.d2p(x1));
D.setStyles(handle, {
'left' : l+'px',
'top' : t+'px',
'width': w+'px'
});
}
function reset() {
if (!this.handles.initialized) return;
var
handles = this.handles;
if (handles) {
D.hide(handles.left);
D.hide(handles.right);
D.hide(handles.scroll);
}
}
function mouseMoveHandler(e, position) {
if (!this.handles.initialized) return;
if (!this.handles.moveHandler) return;
var
delta = position.x - this.lastMousePos.x,
selection = this.selection.selection,
area = this.selection.getArea(),
handles = this.handles;
handles.moveHandler(area, delta);
checkSwap(area, handles);
this.selection.setSelection(area);
}
function checkSwap (area, handles) {
var moveHandler = handles.moveHandler;
if (area.x1 > area.x2) {
if (moveHandler == leftMoveHandler) {
moveHandler = rightMoveHandler;
} else if (moveHandler == rightMoveHandler) {
moveHandler = leftMoveHandler;
}
handles.moveHandler = moveHandler;
}
}
function leftMoveHandler(area, delta) {
area.x1 += delta;
}
function rightMoveHandler(area, delta) {
area.x2 += delta;
}
function scrollMoveHandler(area, delta) {
area.x1 += delta;
area.x2 += delta;
}
})();
/*!
* Bonzo: DOM Utility (c) Dustin Diaz 2011
* https://github.com/ded/bonzo
* License MIT
*/
!function(a,b){typeof define=="function"?define(b):typeof module!="undefined"?module.exports=b():this[a]=b()}("bonzo",function(){function x(a){return new RegExp("(^|\\s+)"+a+"(\\s+|$)")}function y(a,b,c){for(var d=0,e=a.length;d© 2015 - 2025 Weber Informatics LLC | Privacy Policy