diff --git a/etc/excluded b/etc/excluded new file mode 100644 index 0000000..4974b69 --- /dev/null +++ b/etc/excluded @@ -0,0 +1,15 @@ +#------------------------------------------------------------------------------ +# File used to defined which client ip address, network regex address and auth +# login and URI to exclude from report. +# You can define one by line exclusion by specifying first the type of the +# exclusion (USER, CLIENT or URI) and a space separated list of valid regex. +# +# See example bellow: +#------------------------------------------------------------------------------ +#CLIENT 192\.168\.1\.2 +#CLIENT 10\.169\.1\.\d+ 192\.168\.10\..* +#USER myloginstr +#USER guestlogin\d+ guestdemo +#URI http:\/\/myinternetdomain.dom.* +#URI .*\.webmail\.com\/.*\/login\.php.* + diff --git a/etc/network-aliases b/etc/network-aliases new file mode 100644 index 0000000..af3a06d --- /dev/null +++ b/etc/network-aliases @@ -0,0 +1,10 @@ +#------------------------------------------------------------------------------- +# Squid Analyzer Network Alias configuration file +# FORMAT : NETWORK_NAME NETWORK_REGEX1,NETWORK_REGEX2,... +# Field separator must be one or more tabulation. See examples below. +# +#------------------------------------------------------------------------------- +#Home Network 192\.168\.1\. +#Associated Networks ^10\.169\.[2-4]\.,^10\.169\.[7-9]\. +#ClassB Network ^10\.172\. + diff --git a/etc/squidanalyzer.conf b/etc/squidanalyzer.conf new file mode 100644 index 0000000..3e212c2 --- /dev/null +++ b/etc/squidanalyzer.conf @@ -0,0 +1,81 @@ +#### +# This file is the default configuration file for SquidAnalyzer +# Edit it to match your needs and copy it under /etc/squidanalyzer.conf +##### + +# Path where SquidAnalyzer should dump all HTML and images files. +# Choose a path that can be read by a Web browser +Output /var/www/squidanalyzer + +# The URL of the SquidAnalyzer javascript, HTML and images files. +WebUrl /squidreport + +# Set the path to the Squid log file +LogFile /var/log/squid3/access.log + +# Set the file containing network alias name. Network are +# show as Ip addresses so if you want to display name instead +# create a file with this format : +# LOCATION_NAME IP_NETWORK_ADDRESS +# Separator must be a tabulation +NetworkAlias /etc/squidanalyzer/network-aliases + +# Set the file containing user alias name. If you don't have auth_proxy +# enable user are seen as Ip addresses, or if you want to replace login +# name by full user name, create a file with this format : +# FULL_USERNAME IP_ADDRESS || LOGIN_NAME +# Separator must be a tabulation +UserAlias /etc/squidanalyzer/user-aliases + +# How do we sort Network, User and Url report screen +# Value can be: bytes, hits or duration. Default is bytes. +OrderNetwork bytes +OrderUser bytes +OrderUrl bytes + +# How do we sort Mime types report screen +# Value can be: bytes or hits. Default is bytes. +OrderMime bytes + +# Should we display user details. This will show all URL read +# by user. Take care to have enougth space disk for large user. +UrlReport 1 + +# Run in quiet mode or print debug information +QuietMode 1 + +# Cost of the bandwith per Mb. If you want to generate invoice per Mb +# for bandwith traffic this can help you. Value 0 mean no cost. +CostPrice 0.5 + +# Currency of the bandwith cost +Currency € + +# Top number of url to show +TopNumber 100 + +# Path to the file containing client ip addresses, network ip address, +# and/or auth login to exclude from report +Exclude /etc/squidanalyzer/excluded + +# Translation Language file to use (fr_FR.txt, pt_BR.txt and en_US.txt). +# Default to en_US.txt +#Lang /etc/squidanalyzer/lang/fr_FR.txt + +# Date format use to display date (year = %y, month = %m and day = %d) +# You can also use %M to replace month by its 3 letters abbreviation. +DateFormat %y-%m-%d + +# Set this to 1 if you want to anonymize all user login. The username +# will be replaced by an unique id that change at each squid-analyzer +# run. Default disable. +AnonymizeLogin 0 + +# Adds peer cache hit (CD_SIBLING_HIT) to be taken has local cache hit. +# Enabled by default, you must disabled it if you don't want to report +# peer cache hit onto your stats. +SiblingHit 1 + +# Set the image format that should be used in graphics download. Values +# can be png or jpg. Default is png. +ImgFormat png diff --git a/etc/user-aliases b/etc/user-aliases new file mode 100644 index 0000000..b6ac34e --- /dev/null +++ b/etc/user-aliases @@ -0,0 +1,8 @@ +#------------------------------------------------------------------------------- +# Squid Analyzer User Alias configuration file +# FORMAT : FULL_USER_NAME IP_ADDRESS|LOGIN_NAME,LOGIN_REGEX +# Field separator must be one or more tabulation. Space in user name are not +# allowed. See example bellow +#------------------------------------------------------------------------------- +#MyFirstName mylogin,192.168.1.12 +#MyOtherNames logon\d+ diff --git a/resources/back-arrow.png b/resources/back-arrow.png new file mode 100644 index 0000000..c893f0c Binary files /dev/null and b/resources/back-arrow.png differ diff --git a/resources/cursor.png b/resources/cursor.png new file mode 100644 index 0000000..eedbccc Binary files /dev/null and b/resources/cursor.png differ diff --git a/resources/domain.png b/resources/domain.png new file mode 100644 index 0000000..c8cc966 Binary files /dev/null and b/resources/domain.png differ diff --git a/resources/flotr2.js b/resources/flotr2.js new file mode 100644 index 0000000..57bed7a --- /dev/null +++ b/resources/flotr2.js @@ -0,0 +1,6995 @@ +/*! + * 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 +}); +// 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; + }; + +})(); +/** + * 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) && + !v.jquery + ) { + 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._; + +function getEl (el) { + return (el && el.jquery) ? el[0] : el; +} + +Flotr.DOM = { + addClass: function(element, name){ + element = getEl(element); + 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 = getEl(element); + element.innerHTML = ''; + /* + if (!element) return; + _.each(element.childNodes, function (e) { + Flotr.DOM.empty(e); + element.removeChild(e); + }); + */ + }, + remove: function (element) { + element = getEl(element); + element.parentNode.removeChild(element); + }, + hide: function(element){ + element = getEl(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){ + element = getEl(element); + if(_.isString(child)) + element.innerHTML += child; + else if (_.isElement(child)) + element.appendChild(child); + }, + // @TODO find xbrowser implementation + opacity: function(element, opacity) { + element = getEl(element); + element.style.opacity = opacity; + }, + position: function(element, p){ + element = getEl(element); + 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 = getEl(element); + element.className = _.filter(classList.split(/\s+/g), function (c) { + if (c != name) return true; } + ).join(' '); + }, + setStyles: function(element, o) { + element = getEl(element); + _.each(o, function (value, key) { + element.style[key] = value; + }); + }, + show: function(element){ + element = getEl(element); + Flotr.DOM.setStyles(element, {display:''}); + }, + /** + * Return element size. + */ + size: function(element){ + element = getEl(element); + return { + height : element.offsetHeight, + width : element.offsetWidth }; + } +}; + +})(); + +/** + * Flotr Event Adapter + */ +(function () { +var + F = Flotr, + bean = F.bean; +F.EventAdapter = { + observe: function(object, name, callback) { + bean.add(object, name, callback); + return this; + }, + fire: function(object, name, args) { + bean.fire(object, name, args); + if (typeof(Prototype) != 'undefined') + Event.fire(object, name, args); + // @TODO Someone who uses mootools, add mootools adapter for existing applciations. + return this; + }, + stopObserving: function(object, name, callback) { + bean.remove(object, name, callback); + return this; + }, + eventPointer: function(e) { + if (!F._.isUndefined(e.touches) && e.touches.length > 0) { + return { + x : e.touches[0].pageX, + y : e.touches[0].pageY + }; + } else if (!F._.isUndefined(e.changedTouches) && e.changedTouches.length > 0) { + return { + x : e.changedTouches[0].pageX, + y : e.changedTouches[0].pageY + }; + } else if (e.pageX || e.pageY) { + return { + x : e.pageX, + y : e.pageY + }; + } else if (e.clientX || e.clientY) { + var + d = document, + b = d.body, + de = d.documentElement; + return { + x: e.clientX + b.scrollLeft + de.scrollLeft, + y: e.clientY + b.scrollTop + de.scrollTop + }; + } + } +}; +})(); + +/** + * Text Utilities + */ +(function () { + +var + F = Flotr, + D = F.DOM, + _ = F._, + +Text = function (o) { + this.o = o; +}; + +Text.prototype = { + + dimensions : function (text, canvasStyle, htmlStyle, className) { + + if (!text) return { width : 0, height : 0 }; + + return (this.o.html) ? + this.html(text, this.o.element, htmlStyle, className) : + this.canvas(text, canvasStyle); + }, + + canvas : function (text, style) { + + if (!this.o.textEnabled) return; + style = style || {}; + + var + metrics = this.measureText(text, style), + width = metrics.width, + height = style.size || F.defaultOptions.fontSize, + angle = style.angle || 0, + cosAngle = Math.cos(angle), + sinAngle = Math.sin(angle), + widthPadding = 2, + heightPadding = 6, + bounds; + + bounds = { + width: Math.abs(cosAngle * width) + Math.abs(sinAngle * height) + widthPadding, + height: Math.abs(sinAngle * width) + Math.abs(cosAngle * height) + heightPadding + }; + + return bounds; + }, + + html : function (text, element, style, className) { + + var div = D.create('div'); + + D.setStyles(div, { 'position' : 'absolute', 'top' : '-10000px' }); + D.insert(div, '
' + text + '
'); + D.insert(this.o.element, div); + + return D.size(div); + }, + + measureText : function (text, style) { + + var + context = this.o.ctx, + metrics; + + if (!context.fillText || (F.isIphone && context.measure)) { + return { width : context.measure(text, style)}; + } + + style = _.extend({ + size: F.defaultOptions.fontSize, + weight: 1, + angle: 0 + }, style); + + context.save(); + context.font = (style.weight > 1 ? "bold " : "") + (style.size*1.3) + "px sans-serif"; + metrics = context.measureText(text); + context.restore(); + + return metrics; + } +}; + +Flotr.Text = Text; + +})(); + +/** + * Flotr Graph class that plots a graph on creation. + */ +(function () { + +var + D = Flotr.DOM, + E = Flotr.EventAdapter, + _ = Flotr._, + flotr = Flotr; +/** + * Flotr Graph constructor. + * @param {Element} el - element to insert the graph into + * @param {Object} data - an array or object of dataseries + * @param {Object} options - an object containing options + */ +Graph = function(el, data, options){ +// Let's see if we can get away with out this [JS] +// try { + this._setEl(el); + this._initMembers(); + this._initPlugins(); + + E.fire(this.el, 'flotr:beforeinit', [this]); + + this.data = data; + this.series = flotr.Series.getSeries(data); + this._initOptions(options); + this._initGraphTypes(); + this._initCanvas(); + this._text = new flotr.Text({ + element : this.el, + ctx : this.ctx, + html : this.options.HtmlText, + textEnabled : this.textEnabled + }); + E.fire(this.el, 'flotr:afterconstruct', [this]); + this._initEvents(); + + this.findDataRanges(); + this.calculateSpacing(); + + this.draw(_.bind(function() { + E.fire(this.el, 'flotr:afterinit', [this]); + }, this)); +/* + try { + } catch (e) { + try { + console.error(e); + } catch (e2) {} + }*/ +}; + +function observe (object, name, callback) { + E.observe.apply(this, arguments); + this._handles.push(arguments); + return this; +} + +Graph.prototype = { + + destroy: function () { + E.fire(this.el, 'flotr:destroy'); + _.each(this._handles, function (handle) { + E.stopObserving.apply(this, handle); + }); + this._handles = []; + this.el.graph = null; + }, + + observe : observe, + + /** + * @deprecated + */ + _observe : observe, + + processColor: function(color, options){ + var o = { x1: 0, y1: 0, x2: this.plotWidth, y2: this.plotHeight, opacity: 1, ctx: this.ctx }; + _.extend(o, options); + return flotr.Color.processColor(color, o); + }, + /** + * Function determines the min and max values for the xaxis and yaxis. + * + * TODO logarithmic range validation (consideration of 0) + */ + findDataRanges: function(){ + var a = this.axes, + xaxis, yaxis, range; + + _.each(this.series, function (series) { + range = series.getRange(); + if (range) { + xaxis = series.xaxis; + yaxis = series.yaxis; + xaxis.datamin = Math.min(range.xmin, xaxis.datamin); + xaxis.datamax = Math.max(range.xmax, xaxis.datamax); + yaxis.datamin = Math.min(range.ymin, yaxis.datamin); + yaxis.datamax = Math.max(range.ymax, yaxis.datamax); + xaxis.used = (xaxis.used || range.xused); + yaxis.used = (yaxis.used || range.yused); + } + }, this); + + // Check for empty data, no data case (none used) + if (!a.x.used && !a.x2.used) a.x.used = true; + if (!a.y.used && !a.y2.used) a.y.used = true; + + _.each(a, function (axis) { + axis.calculateRange(); + }); + + var + types = _.keys(flotr.graphTypes), + drawn = false; + + _.each(this.series, function (series) { + if (series.hide) return; + _.each(types, function (type) { + if (series[type] && series[type].show) { + this.extendRange(type, series); + drawn = true; + } + }, this); + if (!drawn) { + this.extendRange(this.options.defaultType, series); + } + }, this); + }, + + extendRange : function (type, series) { + if (this[type].extendRange) this[type].extendRange(series, series.data, series[type], this[type]); + if (this[type].extendYRange) this[type].extendYRange(series.yaxis, series.data, series[type], this[type]); + if (this[type].extendXRange) this[type].extendXRange(series.xaxis, series.data, series[type], this[type]); + }, + + /** + * Calculates axis label sizes. + */ + calculateSpacing: function(){ + + var a = this.axes, + options = this.options, + series = this.series, + margin = options.grid.labelMargin, + T = this._text, + x = a.x, + x2 = a.x2, + y = a.y, + y2 = a.y2, + maxOutset = options.grid.outlineWidth, + i, j, l, dim; + + // TODO post refactor, fix this + _.each(a, function (axis) { + axis.calculateTicks(); + axis.calculateTextDimensions(T, options); + }); + + // Title height + dim = T.dimensions( + options.title, + {size: options.fontSize*1.5}, + 'font-size:1em;font-weight:bold;', + 'flotr-title' + ); + this.titleHeight = dim.height; + + // Subtitle height + dim = T.dimensions( + options.subtitle, + {size: options.fontSize}, + 'font-size:smaller;', + 'flotr-subtitle' + ); + this.subtitleHeight = dim.height; + + for(j = 0; j < options.length; ++j){ + if (series[j].points.show){ + maxOutset = Math.max(maxOutset, series[j].points.radius + series[j].points.lineWidth/2); + } + } + + var p = this.plotOffset; + if (x.options.margin === false) { + p.bottom = 0; + p.top = 0; + } else { + p.bottom += (options.grid.circular ? 0 : (x.used && x.options.showLabels ? (x.maxLabel.height + margin) : 0)) + + (x.used && x.options.title ? (x.titleSize.height + margin) : 0) + maxOutset; + + p.top += (options.grid.circular ? 0 : (x2.used && x2.options.showLabels ? (x2.maxLabel.height + margin) : 0)) + + (x2.used && x2.options.title ? (x2.titleSize.height + margin) : 0) + this.subtitleHeight + this.titleHeight + maxOutset; + } + if (y.options.margin === false) { + p.left = 0; + p.right = 0; + } else { + p.left += (options.grid.circular ? 0 : (y.used && y.options.showLabels ? (y.maxLabel.width + margin) : 0)) + + (y.used && y.options.title ? (y.titleSize.width + margin) : 0) + maxOutset; + + p.right += (options.grid.circular ? 0 : (y2.used && y2.options.showLabels ? (y2.maxLabel.width + margin) : 0)) + + (y2.used && y2.options.title ? (y2.titleSize.width + margin) : 0) + maxOutset; + } + + p.top = Math.floor(p.top); // In order the outline not to be blured + + this.plotWidth = this.canvasWidth - p.left - p.right; + this.plotHeight = this.canvasHeight - p.bottom - p.top; + + // TODO post refactor, fix this + x.length = x2.length = this.plotWidth; + y.length = y2.length = this.plotHeight; + y.offset = y2.offset = this.plotHeight; + x.setScale(); + x2.setScale(); + y.setScale(); + y2.setScale(); + }, + /** + * Draws grid, labels, series and outline. + */ + draw: function(after) { + + var + context = this.ctx, + i; + + E.fire(this.el, 'flotr:beforedraw', [this.series, this]); + + if (this.series.length) { + + context.save(); + context.translate(this.plotOffset.left, this.plotOffset.top); + + for (i = 0; i < this.series.length; i++) { + if (!this.series[i].hide) this.drawSeries(this.series[i]); + } + + context.restore(); + this.clip(); + } + + E.fire(this.el, 'flotr:afterdraw', [this.series, this]); + if (after) after(); + }, + /** + * Actually draws the graph. + * @param {Object} series - series to draw + */ + drawSeries: function(series){ + + function drawChart (series, typeKey) { + var options = this.getOptions(series, typeKey); + this[typeKey].draw(options); + } + + var drawn = false; + series = series || this.series; + + _.each(flotr.graphTypes, function (type, typeKey) { + if (series[typeKey] && series[typeKey].show && this[typeKey]) { + drawn = true; + drawChart.call(this, series, typeKey); + } + }, this); + + if (!drawn) drawChart.call(this, series, this.options.defaultType); + }, + + getOptions : function (series, typeKey) { + var + type = series[typeKey], + graphType = this[typeKey], + xaxis = series.xaxis, + yaxis = series.yaxis, + options = { + context : this.ctx, + width : this.plotWidth, + height : this.plotHeight, + fontSize : this.options.fontSize, + fontColor : this.options.fontColor, + textEnabled : this.textEnabled, + htmlText : this.options.HtmlText, + text : this._text, // TODO Is this necessary? + element : this.el, + data : series.data, + color : series.color, + shadowSize : series.shadowSize, + xScale : xaxis.d2p, + yScale : yaxis.d2p, + xInverse : xaxis.p2d, + yInverse : yaxis.p2d + }; + + options = flotr.merge(type, options); + + // Fill + options.fillStyle = this.processColor( + type.fillColor || series.color, + {opacity: type.fillOpacity} + ); + + return options; + }, + /** + * Calculates the coordinates from a mouse event object. + * @param {Event} event - Mouse Event object. + * @return {Object} Object with coordinates of the mouse. + */ + getEventPosition: function (e){ + + var + d = document, + b = d.body, + de = d.documentElement, + axes = this.axes, + plotOffset = this.plotOffset, + lastMousePos = this.lastMousePos, + pointer = E.eventPointer(e), + dx = pointer.x - lastMousePos.pageX, + dy = pointer.y - lastMousePos.pageY, + r, rx, ry; + + if ('ontouchstart' in this.el) { + r = D.position(this.overlay); + rx = pointer.x - r.left - plotOffset.left; + ry = pointer.y - r.top - plotOffset.top; + } else { + r = this.overlay.getBoundingClientRect(); + rx = e.clientX - r.left - plotOffset.left - b.scrollLeft - de.scrollLeft; + ry = e.clientY - r.top - plotOffset.top - b.scrollTop - de.scrollTop; + } + + return { + x: axes.x.p2d(rx), + x2: axes.x2.p2d(rx), + y: axes.y.p2d(ry), + y2: axes.y2.p2d(ry), + relX: rx, + relY: ry, + dX: dx, + dY: dy, + absX: pointer.x, + absY: pointer.y, + pageX: pointer.x, + pageY: pointer.y + }; + }, + /** + * Observes the 'click' event and fires the 'flotr:click' event. + * @param {Event} event - 'click' Event object. + */ + clickHandler: function(event){ + if(this.ignoreClick){ + this.ignoreClick = false; + return this.ignoreClick; + } + E.fire(this.el, 'flotr:click', [this.getEventPosition(event), this]); + }, + /** + * Observes mouse movement over the graph area. Fires the 'flotr:mousemove' event. + * @param {Event} event - 'mousemove' Event object. + */ + mouseMoveHandler: function(event){ + if (this.mouseDownMoveHandler) return; + var pos = this.getEventPosition(event); + E.fire(this.el, 'flotr:mousemove', [event, pos, this]); + this.lastMousePos = pos; + }, + /** + * Observes the 'mousedown' event. + * @param {Event} event - 'mousedown' Event object. + */ + mouseDownHandler: function (event){ + + /* + // @TODO Context menu? + if(event.isRightClick()) { + event.stop(); + + var overlay = this.overlay; + overlay.hide(); + + function cancelContextMenu () { + overlay.show(); + E.stopObserving(document, 'mousemove', cancelContextMenu); + } + E.observe(document, 'mousemove', cancelContextMenu); + return; + } + */ + + if (this.mouseUpHandler) return; + this.mouseUpHandler = _.bind(function (e) { + E.stopObserving(document, 'mouseup', this.mouseUpHandler); + E.stopObserving(document, 'mousemove', this.mouseDownMoveHandler); + this.mouseDownMoveHandler = null; + this.mouseUpHandler = null; + // @TODO why? + //e.stop(); + E.fire(this.el, 'flotr:mouseup', [e, this]); + }, this); + this.mouseDownMoveHandler = _.bind(function (e) { + var pos = this.getEventPosition(e); + E.fire(this.el, 'flotr:mousemove', [event, pos, this]); + this.lastMousePos = pos; + }, this); + E.observe(document, 'mouseup', this.mouseUpHandler); + E.observe(document, 'mousemove', this.mouseDownMoveHandler); + E.fire(this.el, 'flotr:mousedown', [event, this]); + this.ignoreClick = false; + }, + drawTooltip: function(content, x, y, options) { + var mt = this.getMouseTrack(), + style = 'opacity:0.7;background-color:#000;color:#fff;display:none;position:absolute;padding:2px 8px;-moz-border-radius:4px;border-radius:4px;white-space:nowrap;', + p = options.position, + m = options.margin, + plotOffset = this.plotOffset; + + if(x !== null && y !== null){ + if (!options.relative) { // absolute to the canvas + if(p.charAt(0) == 'n') style += 'top:' + (m + plotOffset.top) + 'px;bottom:auto;'; + else if(p.charAt(0) == 's') style += 'bottom:' + (m + plotOffset.bottom) + 'px;top:auto;'; + if(p.charAt(1) == 'e') style += 'right:' + (m + plotOffset.right) + 'px;left:auto;'; + else if(p.charAt(1) == 'w') style += 'left:' + (m + plotOffset.left) + 'px;right:auto;'; + } + else { // relative to the mouse + if(p.charAt(0) == 'n') style += 'bottom:' + (m - plotOffset.top - y + this.canvasHeight) + 'px;top:auto;'; + else if(p.charAt(0) == 's') style += 'top:' + (m + plotOffset.top + y) + 'px;bottom:auto;'; + if(p.charAt(1) == 'e') style += 'left:' + (m + plotOffset.left + x) + 'px;right:auto;'; + else if(p.charAt(1) == 'w') style += 'right:' + (m - plotOffset.left - x + this.canvasWidth) + 'px;left:auto;'; + } + + mt.style.cssText = style; + D.empty(mt); + D.insert(mt, content); + D.show(mt); + } + else { + D.hide(mt); + } + }, + + clip: function (ctx) { + + var + o = this.plotOffset, + w = this.canvasWidth, + h = this.canvasHeight; + + ctx = ctx || this.ctx; + + if (flotr.isIE && flotr.isIE < 9) { + // Clipping for excanvas :-( + ctx.save(); + ctx.fillStyle = this.processColor(this.options.ieBackgroundColor); + ctx.fillRect(0, 0, w, o.top); + ctx.fillRect(0, 0, o.left, h); + ctx.fillRect(0, h - o.bottom, w, o.bottom); + ctx.fillRect(w - o.right, 0, o.right,h); + ctx.restore(); + } else { + ctx.clearRect(0, 0, w, o.top); + ctx.clearRect(0, 0, o.left, h); + ctx.clearRect(0, h - o.bottom, w, o.bottom); + ctx.clearRect(w - o.right, 0, o.right,h); + } + }, + + _initMembers: function() { + this._handles = []; + this.lastMousePos = {pageX: null, pageY: null }; + this.plotOffset = {left: 0, right: 0, top: 0, bottom: 0}; + this.ignoreClick = true; + this.prevHit = null; + }, + + _initGraphTypes: function() { + _.each(flotr.graphTypes, function(handler, graphType){ + this[graphType] = flotr.clone(handler); + }, this); + }, + + _initEvents: function () { + + var + el = this.el, + touchendHandler, movement, touchend; + + if ('ontouchstart' in el) { + + touchendHandler = _.bind(function (e) { + touchend = true; + E.stopObserving(document, 'touchend', touchendHandler); + E.fire(el, 'flotr:mouseup', [event, this]); + this.multitouches = null; + + if (!movement) { + this.clickHandler(e); + } + }, this); + + this.observe(this.overlay, 'touchstart', _.bind(function (e) { + movement = false; + touchend = false; + this.ignoreClick = false; + + if (e.touches && e.touches.length > 1) { + this.multitouches = e.touches; + } + + E.fire(el, 'flotr:mousedown', [event, this]); + this.observe(document, 'touchend', touchendHandler); + }, this)); + + this.observe(this.overlay, 'touchmove', _.bind(function (e) { + + var pos = this.getEventPosition(e); + + if (this.options.preventDefault) { + e.preventDefault(); + } + + movement = true; + + if (this.multitouches || (e.touches && e.touches.length > 1)) { + this.multitouches = e.touches; + } else { + if (!touchend) { + E.fire(el, 'flotr:mousemove', [event, pos, this]); + } + } + this.lastMousePos = pos; + }, this)); + + } else { + this. + observe(this.overlay, 'mousedown', _.bind(this.mouseDownHandler, this)). + observe(el, 'mousemove', _.bind(this.mouseMoveHandler, this)). + observe(this.overlay, 'click', _.bind(this.clickHandler, this)). + observe(el, 'mouseout', function () { + E.fire(el, 'flotr:mouseout'); + }); + } + }, + + /** + * Initializes the canvas and it's overlay canvas element. When the browser is IE, this makes use + * of excanvas. The overlay canvas is inserted for displaying interactions. After the canvas elements + * are created, the elements are inserted into the container element. + */ + _initCanvas: function(){ + var el = this.el, + o = this.options, + children = el.children, + removedChildren = [], + child, i, + size, style; + + // Empty the el + for (i = children.length; i--;) { + child = children[i]; + if (!this.canvas && child.className === 'flotr-canvas') { + this.canvas = child; + } else if (!this.overlay && child.className === 'flotr-overlay') { + this.overlay = child; + } else { + removedChildren.push(child); + } + } + for (i = removedChildren.length; i--;) { + el.removeChild(removedChildren[i]); + } + + D.setStyles(el, {position: 'relative'}); // For positioning labels and overlay. + size = {}; + size.width = el.clientWidth; + size.height = el.clientHeight; + + if(size.width <= 0 || size.height <= 0 || o.resolution <= 0){ + throw 'Invalid dimensions for plot, width = ' + size.width + ', height = ' + size.height + ', resolution = ' + o.resolution; + } + + // Main canvas for drawing graph types + this.canvas = getCanvas(this.canvas, 'canvas'); + // Overlay canvas for interactive features + this.overlay = getCanvas(this.overlay, 'overlay'); + this.ctx = getContext(this.canvas); + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + this.octx = getContext(this.overlay); + this.octx.clearRect(0, 0, this.overlay.width, this.overlay.height); + this.canvasHeight = size.height; + this.canvasWidth = size.width; + this.textEnabled = !!this.ctx.drawText || !!this.ctx.fillText; // Enable text functions + + function getCanvas(canvas, name){ + if(!canvas){ + canvas = D.create('canvas'); + if (typeof FlashCanvas != "undefined" && typeof canvas.getContext === 'function') { + FlashCanvas.initElement(canvas); + } + canvas.className = 'flotr-'+name; + canvas.style.cssText = 'position:absolute;left:0px;top:0px;'; + D.insert(el, canvas); + } + _.each(size, function(size, attribute){ + D.show(canvas); + if (name == 'canvas' && canvas.getAttribute(attribute) === size) { + return; + } + canvas.setAttribute(attribute, size * o.resolution); + canvas.style[attribute] = size + 'px'; + }); + canvas.context_ = null; // Reset the ExCanvas context + return canvas; + } + + function getContext(canvas){ + if(window.G_vmlCanvasManager) window.G_vmlCanvasManager.initElement(canvas); // For ExCanvas + var context = canvas.getContext('2d'); + if(!window.G_vmlCanvasManager) context.scale(o.resolution, o.resolution); + return context; + } + }, + + _initPlugins: function(){ + // TODO Should be moved to flotr and mixed in. + _.each(flotr.plugins, function(plugin, name){ + _.each(plugin.callbacks, function(fn, c){ + this.observe(this.el, c, _.bind(fn, this)); + }, this); + this[name] = flotr.clone(plugin); + _.each(this[name], function(fn, p){ + if (_.isFunction(fn)) + this[name][p] = _.bind(fn, this); + }, this); + }, this); + }, + + /** + * Sets options and initializes some variables and color specific values, used by the constructor. + * @param {Object} opts - options object + */ + _initOptions: function(opts){ + var options = flotr.clone(flotr.defaultOptions); + options.x2axis = _.extend(_.clone(options.xaxis), options.x2axis); + options.y2axis = _.extend(_.clone(options.yaxis), options.y2axis); + this.options = flotr.merge(opts || {}, options); + + if (this.options.grid.minorVerticalLines === null && + this.options.xaxis.scaling === 'logarithmic') { + this.options.grid.minorVerticalLines = true; + } + if (this.options.grid.minorHorizontalLines === null && + this.options.yaxis.scaling === 'logarithmic') { + this.options.grid.minorHorizontalLines = true; + } + + E.fire(this.el, 'flotr:afterinitoptions', [this]); + + this.axes = flotr.Axis.getAxes(this.options); + + // Initialize some variables used throughout this function. + var assignedColors = [], + colors = [], + ln = this.series.length, + neededColors = this.series.length, + oc = this.options.colors, + usedColors = [], + variation = 0, + c, i, j, s; + + // Collect user-defined colors from series. + for(i = neededColors - 1; i > -1; --i){ + c = this.series[i].color; + if(c){ + --neededColors; + if(_.isNumber(c)) assignedColors.push(c); + else usedColors.push(flotr.Color.parse(c)); + } + } + + // Calculate the number of colors that need to be generated. + for(i = assignedColors.length - 1; i > -1; --i) + neededColors = Math.max(neededColors, assignedColors[i] + 1); + + // Generate needed number of colors. + for(i = 0; colors.length < neededColors;){ + c = (oc.length == i) ? new flotr.Color(100, 100, 100) : flotr.Color.parse(oc[i]); + + // Make sure each serie gets a different color. + var sign = variation % 2 == 1 ? -1 : 1, + factor = 1 + sign * Math.ceil(variation / 2) * 0.2; + c.scale(factor, factor, factor); + + /** + * @todo if we're getting too close to something else, we should probably skip this one + */ + colors.push(c); + + if(++i >= oc.length){ + i = 0; + ++variation; + } + } + + // Fill the options with the generated colors. + for(i = 0, j = 0; i < ln; ++i){ + s = this.series[i]; + + // Assign the color. + if (!s.color){ + s.color = colors[j++].toString(); + }else if(_.isNumber(s.color)){ + s.color = colors[s.color].toString(); + } + + // Every series needs an axis + if (!s.xaxis) s.xaxis = this.axes.x; + if (s.xaxis == 1) s.xaxis = this.axes.x; + else if (s.xaxis == 2) s.xaxis = this.axes.x2; + + if (!s.yaxis) s.yaxis = this.axes.y; + if (s.yaxis == 1) s.yaxis = this.axes.y; + else if (s.yaxis == 2) s.yaxis = this.axes.y2; + + // Apply missing options to the series. + for (var t in flotr.graphTypes){ + s[t] = _.extend(_.clone(this.options[t]), s[t]); + } + s.mouse = _.extend(_.clone(this.options.mouse), s.mouse); + + if (_.isUndefined(s.shadowSize)) s.shadowSize = this.options.shadowSize; + } + }, + + _setEl: function(el) { + if (!el) throw 'The target container doesn\'t exist'; + else if (el.graph instanceof Graph) el.graph.destroy(); + else if (!el.clientWidth) throw 'The target container must be visible'; + + el.graph = this; + this.el = el; + } +}; + +Flotr.Graph = Graph; + +})(); + +/** + * Flotr Axis Library + */ + +(function () { + +var + _ = Flotr._, + LOGARITHMIC = 'logarithmic'; + +function Axis (o) { + + this.orientation = 1; + this.offset = 0; + this.datamin = Number.MAX_VALUE; + this.datamax = -Number.MAX_VALUE; + + _.extend(this, o); +} + + +// Prototype +Axis.prototype = { + + setScale : function () { + var + length = this.length, + max = this.max, + min = this.min, + offset = this.offset, + orientation = this.orientation, + options = this.options, + logarithmic = options.scaling === LOGARITHMIC, + scale; + + if (logarithmic) { + scale = length / (log(max, options.base) - log(min, options.base)); + } else { + scale = length / (max - min); + } + this.scale = scale; + + // Logarithmic? + if (logarithmic) { + this.d2p = function (dataValue) { + return offset + orientation * (log(dataValue, options.base) - log(min, options.base)) * scale; + }; + this.p2d = function (pointValue) { + return exp((offset + orientation * pointValue) / scale + log(min, options.base), options.base); + }; + } else { + this.d2p = function (dataValue) { + return offset + orientation * (dataValue - min) * scale; + }; + this.p2d = function (pointValue) { + return (offset + orientation * pointValue) / scale + min; + }; + } + }, + + calculateTicks : function () { + var options = this.options; + + this.ticks = []; + this.minorTicks = []; + + // User Ticks + if(options.ticks){ + this._cleanUserTicks(options.ticks, this.ticks); + this._cleanUserTicks(options.minorTicks || [], this.minorTicks); + } + else { + if (options.mode == 'time') { + this._calculateTimeTicks(); + } else if (options.scaling === 'logarithmic') { + this._calculateLogTicks(); + } else { + this._calculateTicks(); + } + } + + // Ticks to strings + _.each(this.ticks, function (tick) { tick.label += ''; }); + _.each(this.minorTicks, function (tick) { tick.label += ''; }); + }, + + /** + * Calculates the range of an axis to apply autoscaling. + */ + calculateRange: function () { + + if (!this.used) return; + + var axis = this, + o = axis.options, + min = o.min !== null ? o.min : axis.datamin, + max = o.max !== null ? o.max : axis.datamax, + margin = o.autoscaleMargin; + + if (o.scaling == 'logarithmic') { + if (min <= 0) min = axis.datamin; + + // Let it widen later on + if (max <= 0) max = min; + } + + if (max == min) { + var widen = max ? 0.01 : 1.00; + if (o.min === null) min -= widen; + if (o.max === null) max += widen; + } + + if (o.scaling === 'logarithmic') { + if (min < 0) min = max / o.base; // Could be the result of widening + + var maxexp = Math.log(max); + if (o.base != Math.E) maxexp /= Math.log(o.base); + maxexp = Math.ceil(maxexp); + + var minexp = Math.log(min); + if (o.base != Math.E) minexp /= Math.log(o.base); + minexp = Math.ceil(minexp); + + axis.tickSize = Flotr.getTickSize(o.noTicks, minexp, maxexp, o.tickDecimals === null ? 0 : o.tickDecimals); + + // Try to determine a suitable amount of miniticks based on the length of a decade + if (o.minorTickFreq === null) { + if (maxexp - minexp > 10) + o.minorTickFreq = 0; + else if (maxexp - minexp > 5) + o.minorTickFreq = 2; + else + o.minorTickFreq = 5; + } + } else { + axis.tickSize = Flotr.getTickSize(o.noTicks, min, max, o.tickDecimals); + } + + axis.min = min; + axis.max = max; //extendRange may use axis.min or axis.max, so it should be set before it is caled + + // Autoscaling. @todo This probably fails with log scale. Find a testcase and fix it + if(o.min === null && o.autoscale){ + axis.min -= axis.tickSize * margin; + // Make sure we don't go below zero if all values are positive. + if(axis.min < 0 && axis.datamin >= 0) axis.min = 0; + axis.min = axis.tickSize * Math.floor(axis.min / axis.tickSize); + } + + if(o.max === null && o.autoscale){ + axis.max += axis.tickSize * margin; + if(axis.max > 0 && axis.datamax <= 0 && axis.datamax != axis.datamin) axis.max = 0; + axis.max = axis.tickSize * Math.ceil(axis.max / axis.tickSize); + } + + if (axis.min == axis.max) axis.max = axis.min + 1; + }, + + calculateTextDimensions : function (T, options) { + + var maxLabel = '', + length, + i; + + if (this.options.showLabels) { + for (i = 0; i < this.ticks.length; ++i) { + length = this.ticks[i].label.length; + if (length > maxLabel.length){ + maxLabel = this.ticks[i].label; + } + } + } + + this.maxLabel = T.dimensions( + maxLabel, + {size:options.fontSize, angle: Flotr.toRad(this.options.labelsAngle)}, + 'font-size:smaller;', + 'flotr-grid-label' + ); + + this.titleSize = T.dimensions( + this.options.title, + {size:options.fontSize*1.2, angle: Flotr.toRad(this.options.titleAngle)}, + 'font-weight:bold;', + 'flotr-axis-title' + ); + }, + + _cleanUserTicks : function (ticks, axisTicks) { + + var axis = this, options = this.options, + v, i, label, tick; + + if(_.isFunction(ticks)) ticks = ticks({min : axis.min, max : axis.max}); + + for(i = 0; i < ticks.length; ++i){ + tick = ticks[i]; + if(typeof(tick) === 'object'){ + v = tick[0]; + label = (tick.length > 1) ? tick[1] : options.tickFormatter(v, {min : axis.min, max : axis.max}); + } else { + v = tick; + label = options.tickFormatter(v, {min : this.min, max : this.max}); + } + axisTicks[i] = { v: v, label: label }; + } + }, + + _calculateTimeTicks : function () { + this.ticks = Flotr.Date.generator(this); + }, + + _calculateLogTicks : function () { + + var axis = this, + o = axis.options, + v, + decadeStart; + + var max = Math.log(axis.max); + if (o.base != Math.E) max /= Math.log(o.base); + max = Math.ceil(max); + + var min = Math.log(axis.min); + if (o.base != Math.E) min /= Math.log(o.base); + min = Math.ceil(min); + + for (i = min; i < max; i += axis.tickSize) { + decadeStart = (o.base == Math.E) ? Math.exp(i) : Math.pow(o.base, i); + // Next decade begins here: + var decadeEnd = decadeStart * ((o.base == Math.E) ? Math.exp(axis.tickSize) : Math.pow(o.base, axis.tickSize)); + var stepSize = (decadeEnd - decadeStart) / o.minorTickFreq; + + axis.ticks.push({v: decadeStart, label: o.tickFormatter(decadeStart, {min : axis.min, max : axis.max})}); + for (v = decadeStart + stepSize; v < decadeEnd; v += stepSize) + axis.minorTicks.push({v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max})}); + } + + // Always show the value at the would-be start of next decade (end of this decade) + decadeStart = (o.base == Math.E) ? Math.exp(i) : Math.pow(o.base, i); + axis.ticks.push({v: decadeStart, label: o.tickFormatter(decadeStart, {min : axis.min, max : axis.max})}); + }, + + _calculateTicks : function () { + + var axis = this, + o = axis.options, + tickSize = axis.tickSize, + min = axis.min, + max = axis.max, + start = tickSize * Math.ceil(min / tickSize), // Round to nearest multiple of tick size. + decimals, + minorTickSize, + v, v2, + i, j; + + if (o.minorTickFreq) + minorTickSize = tickSize / o.minorTickFreq; + + // Then store all possible ticks. + for (i = 0; (v = v2 = start + i * tickSize) <= max; ++i){ + + // Round (this is always needed to fix numerical instability). + decimals = o.tickDecimals; + if (decimals === null) decimals = 1 - Math.floor(Math.log(tickSize) / Math.LN10); + if (decimals < 0) decimals = 0; + + v = v.toFixed(decimals); + axis.ticks.push({ v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max}) }); + + if (o.minorTickFreq) { + for (j = 0; j < o.minorTickFreq && (i * tickSize + j * minorTickSize) < max; ++j) { + v = v2 + j * minorTickSize; + axis.minorTicks.push({ v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max}) }); + } + } + } + + } +}; + + +// Static Methods +_.extend(Axis, { + getAxes : function (options) { + return { + x: new Axis({options: options.xaxis, n: 1, length: this.plotWidth}), + x2: new Axis({options: options.x2axis, n: 2, length: this.plotWidth}), + y: new Axis({options: options.yaxis, n: 1, length: this.plotHeight, offset: this.plotHeight, orientation: -1}), + y2: new Axis({options: options.y2axis, n: 2, length: this.plotHeight, offset: this.plotHeight, orientation: -1}) + }; + } +}); + + +// Helper Methods + + +function log (value, base) { + value = Math.log(Math.max(value, Number.MIN_VALUE)); + if (base !== Math.E) + value /= Math.log(base); + return value; +} + +function exp (value, base) { + return (base === Math.E) ? Math.exp(value) : Math.pow(base, value); +} + +Flotr.Axis = Axis; + +})(); + +/** + * Flotr Series Library + */ + +(function () { + +var + _ = Flotr._; + +function Series (o) { + _.extend(this, o); +} + +Series.prototype = { + + getRange: function () { + + var + data = this.data, + length = data.length, + xmin = Number.MAX_VALUE, + ymin = Number.MAX_VALUE, + xmax = -Number.MAX_VALUE, + ymax = -Number.MAX_VALUE, + xused = false, + yused = false, + x, y, i; + + if (length < 0 || this.hide) return false; + + for (i = 0; i < length; i++) { + x = data[i][0]; + y = data[i][1]; + if (x !== null) { + if (x < xmin) { xmin = x; xused = true; } + if (x > xmax) { xmax = x; xused = true; } + } + if (y !== null) { + if (y < ymin) { ymin = y; yused = true; } + if (y > ymax) { ymax = y; yused = true; } + } + } + + return { + xmin : xmin, + xmax : xmax, + ymin : ymin, + ymax : ymax, + xused : xused, + yused : yused + }; + } +}; + +_.extend(Series, { + /** + * Collects dataseries from input and parses the series into the right format. It returns an Array + * of Objects each having at least the 'data' key set. + * @param {Array, Object} data - Object or array of dataseries + * @return {Array} Array of Objects parsed into the right format ({(...,) data: [[x1,y1], [x2,y2], ...] (, ...)}) + */ + getSeries: function(data){ + return _.map(data, function(s){ + var series; + if (s.data) { + series = new Series(); + _.extend(series, s); + } else { + series = new Series({data:s}); + } + return series; + }); + } +}); + +Flotr.Series = Series; + +})(); + +/** Lines **/ +Flotr.addType('lines', { + options: { + show: false, // => setting to true will show lines, false will hide + lineWidth: 2, // => line width in pixels + fill: false, // => true to fill the area from the line to the x axis, false for (transparent) no fill + fillBorder: false, // => draw a border around the fill + fillColor: null, // => fill color + fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill + steps: false, // => draw steps + stacked: false // => setting to true will show stacked lines, false will show normal lines + }, + + stack : { + values : [] + }, + + /** + * Draws lines series in the canvas element. + * @param {Object} options + */ + draw : function (options) { + + var + context = options.context, + lineWidth = options.lineWidth, + shadowSize = options.shadowSize, + offset; + + context.save(); + context.lineJoin = 'round'; + + if (shadowSize) { + + context.lineWidth = shadowSize / 2; + offset = lineWidth / 2 + context.lineWidth / 2; + + // @TODO do this instead with a linear gradient + context.strokeStyle = "rgba(0,0,0,0.1)"; + this.plot(options, offset + shadowSize / 2, false); + + context.strokeStyle = "rgba(0,0,0,0.2)"; + this.plot(options, offset, false); + } + + context.lineWidth = lineWidth; + context.strokeStyle = options.color; + + this.plot(options, 0, true); + + context.restore(); + }, + + plot : function (options, shadowOffset, incStack) { + + var + context = options.context, + width = options.width, + height = options.height, + xScale = options.xScale, + yScale = options.yScale, + data = options.data, + stack = options.stacked ? this.stack : false, + length = data.length - 1, + prevx = null, + prevy = null, + zero = yScale(0), + start = null, + x1, x2, y1, y2, stack1, stack2, i; + + if (length < 1) return; + + context.beginPath(); + + for (i = 0; i < length; ++i) { + + // To allow empty values + if (data[i][1] === null || data[i+1][1] === null) { + if (options.fill) { + if (i > 0 && data[i][1]) { + context.stroke(); + fill(); + start = null; + context.closePath(); + context.beginPath(); + } + } + continue; + } + + // Zero is infinity for log scales + // TODO handle zero for logarithmic + // if (xa.options.scaling === 'logarithmic' && (data[i][0] <= 0 || data[i+1][0] <= 0)) continue; + // if (ya.options.scaling === 'logarithmic' && (data[i][1] <= 0 || data[i+1][1] <= 0)) continue; + + x1 = xScale(data[i][0]); + x2 = xScale(data[i+1][0]); + + if (start === null) start = data[i]; + + if (stack) { + + stack1 = stack.values[data[i][0]] || 0; + stack2 = stack.values[data[i+1][0]] || stack.values[data[i][0]] || 0; + + y1 = yScale(data[i][1] + stack1); + y2 = yScale(data[i+1][1] + stack2); + + if(incStack){ + stack.values[data[i][0]] = data[i][1]+stack1; + + if(i == length-1) + stack.values[data[i+1][0]] = data[i+1][1]+stack2; + } + } + else{ + y1 = yScale(data[i][1]); + y2 = yScale(data[i+1][1]); + } + + if ( + (y1 > height && y2 > height) || + (y1 < 0 && y2 < 0) || + (x1 < 0 && x2 < 0) || + (x1 > width && x2 > width) + ) continue; + + if((prevx != x1) || (prevy != y1 + shadowOffset)) + context.moveTo(x1, y1 + shadowOffset); + + prevx = x2; + prevy = y2 + shadowOffset; + if (options.steps) { + context.lineTo(prevx + shadowOffset / 2, y1 + shadowOffset); + context.lineTo(prevx + shadowOffset / 2, prevy); + } else { + context.lineTo(prevx, prevy); + } + } + + if (!options.fill || options.fill && !options.fillBorder) context.stroke(); + + fill(); + + function fill () { + // TODO stacked lines + if(!shadowOffset && options.fill && start){ + x1 = xScale(start[0]); + context.fillStyle = options.fillStyle; + context.lineTo(x2, zero); + context.lineTo(x1, zero); + context.lineTo(x1, yScale(start[1])); + context.fill(); + if (options.fillBorder) { + context.stroke(); + } + } + } + + context.closePath(); + }, + + // Perform any pre-render precalculations (this should be run on data first) + // - Pie chart total for calculating measures + // - Stacks for lines and bars + // precalculate : function () { + // } + // + // + // Get any bounds after pre calculation (axis can fetch this if does not have explicit min/max) + // getBounds : function () { + // } + // getMin : function () { + // } + // getMax : function () { + // } + // + // + // Padding around rendered elements + // getPadding : function () { + // } + + extendYRange : function (axis, data, options, lines) { + + var o = axis.options; + + // If stacked and auto-min + if (options.stacked && ((!o.max && o.max !== 0) || (!o.min && o.min !== 0))) { + + var + newmax = axis.max, + newmin = axis.min, + positiveSums = lines.positiveSums || {}, + negativeSums = lines.negativeSums || {}, + x, j; + + for (j = 0; j < data.length; j++) { + + x = data[j][0] + ''; + + // Positive + if (data[j][1] > 0) { + positiveSums[x] = (positiveSums[x] || 0) + data[j][1]; + newmax = Math.max(newmax, positiveSums[x]); + } + + // Negative + else { + negativeSums[x] = (negativeSums[x] || 0) + data[j][1]; + newmin = Math.min(newmin, negativeSums[x]); + } + } + + lines.negativeSums = negativeSums; + lines.positiveSums = positiveSums; + + axis.max = newmax; + axis.min = newmin; + } + + if (options.steps) { + + this.hit = function (options) { + var + data = options.data, + args = options.args, + yScale = options.yScale, + mouse = args[0], + length = data.length, + n = args[1], + x = options.xInverse(mouse.relX), + relY = mouse.relY, + i; + + for (i = 0; i < length - 1; i++) { + if (x >= data[i][0] && x <= data[i+1][0]) { + if (Math.abs(yScale(data[i][1]) - relY) < 8) { + n.x = data[i][0]; + n.y = data[i][1]; + n.index = i; + n.seriesIndex = options.index; + } + break; + } + } + }; + + this.drawHit = function (options) { + var + context = options.context, + args = options.args, + data = options.data, + xScale = options.xScale, + index = args.index, + x = xScale(args.x), + y = options.yScale(args.y), + x2; + + if (data.length - 1 > index) { + x2 = options.xScale(data[index + 1][0]); + context.save(); + context.strokeStyle = options.color; + context.lineWidth = options.lineWidth; + context.beginPath(); + context.moveTo(x, y); + context.lineTo(x2, y); + context.stroke(); + context.closePath(); + context.restore(); + } + }; + + this.clearHit = function (options) { + var + context = options.context, + args = options.args, + data = options.data, + xScale = options.xScale, + width = options.lineWidth, + index = args.index, + x = xScale(args.x), + y = options.yScale(args.y), + x2; + + if (data.length - 1 > index) { + x2 = options.xScale(data[index + 1][0]); + context.clearRect(x - width, y - width, x2 - x + 2 * width, 2 * width); + } + }; + } + } + +}); + +/** Bars **/ +Flotr.addType('bars', { + + options: { + show: false, // => setting to true will show bars, false will hide + lineWidth: 2, // => in pixels + barWidth: 1, // => in units of the x axis + fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill + fillColor: null, // => fill color + fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill + horizontal: false, // => horizontal bars (x and y inverted) + stacked: false, // => stacked bar charts + centered: true, // => center the bars to their x axis value + topPadding: 0.1, // => top padding in percent + grouped: false // => groups bars together which share x value, hit not supported. + }, + + stack : { + positive : [], + negative : [], + _positive : [], // Shadow + _negative : [] // Shadow + }, + + draw : function (options) { + var + context = options.context; + + this.current += 1; + + context.save(); + context.lineJoin = 'miter'; + // @TODO linewidth not interpreted the right way. + context.lineWidth = options.lineWidth; + context.strokeStyle = options.color; + if (options.fill) context.fillStyle = options.fillStyle; + + this.plot(options); + + context.restore(); + }, + + plot : function (options) { + + var + data = options.data, + context = options.context, + shadowSize = options.shadowSize, + i, geometry, left, top, width, height; + + if (data.length < 1) return; + + this.translate(context, options.horizontal); + + for (i = 0; i < data.length; i++) { + + geometry = this.getBarGeometry(data[i][0], data[i][1], options); + if (geometry === null) continue; + + left = geometry.left; + top = geometry.top; + width = geometry.width; + height = geometry.height; + + if (options.fill) context.fillRect(left, top, width, height); + if (shadowSize) { + context.save(); + context.fillStyle = 'rgba(0,0,0,0.05)'; + context.fillRect(left + shadowSize, top + shadowSize, width, height); + context.restore(); + } + if (options.lineWidth) { + context.strokeRect(left, top, width, height); + } + } + }, + + translate : function (context, horizontal) { + if (horizontal) { + context.rotate(-Math.PI / 2); + context.scale(-1, 1); + } + }, + + getBarGeometry : function (x, y, options) { + + var + horizontal = options.horizontal, + barWidth = options.barWidth, + centered = options.centered, + stack = options.stacked ? this.stack : false, + lineWidth = options.lineWidth, + bisection = centered ? barWidth / 2 : 0, + xScale = horizontal ? options.yScale : options.xScale, + yScale = horizontal ? options.xScale : options.yScale, + xValue = horizontal ? y : x, + yValue = horizontal ? x : y, + stackOffset = 0, + stackValue, left, right, top, bottom; + + if (options.grouped) { + this.current / this.groups; + xValue = xValue - bisection; + barWidth = barWidth / this.groups; + bisection = barWidth / 2; + xValue = xValue + barWidth * this.current - bisection; + } + + // Stacked bars + if (stack) { + stackValue = yValue > 0 ? stack.positive : stack.negative; + stackOffset = stackValue[xValue] || stackOffset; + stackValue[xValue] = stackOffset + yValue; + } + + left = xScale(xValue - bisection); + right = xScale(xValue + barWidth - bisection); + top = yScale(yValue + stackOffset); + bottom = yScale(stackOffset); + + // TODO for test passing... probably looks better without this + if (bottom < 0) bottom = 0; + + // TODO Skipping... + // if (right < xa.min || left > xa.max || top < ya.min || bottom > ya.max) continue; + + return (x === null || y === null) ? null : { + x : xValue, + y : yValue, + xScale : xScale, + yScale : yScale, + top : top, + left : Math.min(left, right) - lineWidth / 2, + width : Math.abs(right - left) - lineWidth, + height : bottom - top + }; + }, + + hit : function (options) { + var + data = options.data, + args = options.args, + mouse = args[0], + n = args[1], + x = options.xInverse(mouse.relX), + y = options.yInverse(mouse.relY), + hitGeometry = this.getBarGeometry(x, y, options), + width = hitGeometry.width / 2, + left = hitGeometry.left, + height = hitGeometry.y, + geometry, i; + + for (i = data.length; i--;) { + geometry = this.getBarGeometry(data[i][0], data[i][1], options); + if ( + // Height: + ( + // Positive Bars: + (height > 0 && height < geometry.y) || + // Negative Bars: + (height < 0 && height > geometry.y) + ) && + // Width: + (Math.abs(left - geometry.left) < width) + ) { + n.x = data[i][0]; + n.y = data[i][1]; + n.index = i; + n.seriesIndex = options.index; + } + } + }, + + drawHit : function (options) { + // TODO hits for stacked bars; implement using calculateStack option? + var + context = options.context, + args = options.args, + geometry = this.getBarGeometry(args.x, args.y, options), + left = geometry.left, + top = geometry.top, + width = geometry.width, + height = geometry.height; + + context.save(); + context.strokeStyle = options.color; + context.lineWidth = options.lineWidth; + this.translate(context, options.horizontal); + + // Draw highlight + context.beginPath(); + context.moveTo(left, top + height); + context.lineTo(left, top); + context.lineTo(left + width, top); + context.lineTo(left + width, top + height); + if (options.fill) { + context.fillStyle = options.fillStyle; + context.fill(); + } + context.stroke(); + context.closePath(); + + context.restore(); + }, + + clearHit: function (options) { + var + context = options.context, + args = options.args, + geometry = this.getBarGeometry(args.x, args.y, options), + left = geometry.left, + width = geometry.width, + top = geometry.top, + height = geometry.height, + lineWidth = 2 * options.lineWidth; + + context.save(); + this.translate(context, options.horizontal); + context.clearRect( + left - lineWidth, + Math.min(top, top + height) - lineWidth, + width + 2 * lineWidth, + Math.abs(height) + 2 * lineWidth + ); + context.restore(); + }, + + extendXRange : function (axis, data, options, bars) { + this._extendRange(axis, data, options, bars); + this.groups = (this.groups + 1) || 1; + this.current = 0; + }, + + extendYRange : function (axis, data, options, bars) { + this._extendRange(axis, data, options, bars); + }, + _extendRange: function (axis, data, options, bars) { + + var + max = axis.options.max; + + if (_.isNumber(max) || _.isString(max)) return; + + var + newmin = axis.min, + newmax = axis.max, + horizontal = options.horizontal, + orientation = axis.orientation, + positiveSums = this.positiveSums || {}, + negativeSums = this.negativeSums || {}, + value, datum, index, j; + + // Sides of bars + if ((orientation == 1 && !horizontal) || (orientation == -1 && horizontal)) { + if (options.centered) { + newmax = Math.max(axis.datamax + options.barWidth, newmax); + newmin = Math.min(axis.datamin - options.barWidth, newmin); + } + } + + if (options.stacked && + ((orientation == 1 && horizontal) || (orientation == -1 && !horizontal))){ + + for (j = data.length; j--;) { + value = data[j][(orientation == 1 ? 1 : 0)]+''; + datum = data[j][(orientation == 1 ? 0 : 1)]; + + // Positive + if (datum > 0) { + positiveSums[value] = (positiveSums[value] || 0) + datum; + newmax = Math.max(newmax, positiveSums[value]); + } + + // Negative + else { + negativeSums[value] = (negativeSums[value] || 0) + datum; + newmin = Math.min(newmin, negativeSums[value]); + } + } + } + + // End of bars + if ((orientation == 1 && horizontal) || (orientation == -1 && !horizontal)) { + if (options.topPadding && (axis.max === axis.datamax || (options.stacked && this.stackMax !== newmax))) { + newmax += options.topPadding * (newmax - newmin); + } + } + + this.stackMin = newmin; + this.stackMax = newmax; + this.negativeSums = negativeSums; + this.positiveSums = positiveSums; + + axis.max = newmax; + axis.min = newmin; + } + +}); + +/** Bubbles **/ +Flotr.addType('bubbles', { + options: { + show: false, // => setting to true will show radar chart, false will hide + lineWidth: 2, // => line width in pixels + fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill + fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill + baseRadius: 2 // => ratio of the radar, against the plot size + }, + draw : function (options) { + var + context = options.context, + shadowSize = options.shadowSize; + + context.save(); + context.lineWidth = options.lineWidth; + + // Shadows + context.fillStyle = 'rgba(0,0,0,0.05)'; + context.strokeStyle = 'rgba(0,0,0,0.05)'; + this.plot(options, shadowSize / 2); + context.strokeStyle = 'rgba(0,0,0,0.1)'; + this.plot(options, shadowSize / 4); + + // Chart + context.strokeStyle = options.color; + context.fillStyle = options.fillStyle; + this.plot(options); + + context.restore(); + }, + plot : function (options, offset) { + + var + data = options.data, + context = options.context, + geometry, + i, x, y, z; + + offset = offset || 0; + + for (i = 0; i < data.length; ++i){ + + geometry = this.getGeometry(data[i], options); + + context.beginPath(); + context.arc(geometry.x + offset, geometry.y + offset, geometry.z, 0, 2 * Math.PI, true); + context.stroke(); + if (options.fill) context.fill(); + context.closePath(); + } + }, + getGeometry : function (point, options) { + return { + x : options.xScale(point[0]), + y : options.yScale(point[1]), + z : point[2] * options.baseRadius + }; + }, + hit : function (options) { + var + data = options.data, + args = options.args, + mouse = args[0], + n = args[1], + relX = mouse.relX, + relY = mouse.relY, + distance, + geometry, + dx, dy; + + n.best = n.best || Number.MAX_VALUE; + + for (i = data.length; i--;) { + geometry = this.getGeometry(data[i], options); + + dx = geometry.x - relX; + dy = geometry.y - relY; + distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < geometry.z && geometry.z < n.best) { + n.x = data[i][0]; + n.y = data[i][1]; + n.index = i; + n.seriesIndex = options.index; + n.best = geometry.z; + } + } + }, + drawHit : function (options) { + + var + context = options.context, + geometry = this.getGeometry(options.data[options.args.index], options); + + context.save(); + context.lineWidth = options.lineWidth; + context.fillStyle = options.fillStyle; + context.strokeStyle = options.color; + context.beginPath(); + context.arc(geometry.x, geometry.y, geometry.z, 0, 2 * Math.PI, true); + context.fill(); + context.stroke(); + context.closePath(); + context.restore(); + }, + clearHit : function (options) { + + var + context = options.context, + geometry = this.getGeometry(options.data[options.args.index], options), + offset = geometry.z + options.lineWidth; + + context.save(); + context.clearRect( + geometry.x - offset, + geometry.y - offset, + 2 * offset, + 2 * offset + ); + context.restore(); + } + // TODO Add a hit calculation method (like pie) +}); + +/** Candles **/ +Flotr.addType('candles', { + options: { + show: false, // => setting to true will show candle sticks, false will hide + lineWidth: 1, // => in pixels + wickLineWidth: 1, // => in pixels + candleWidth: 0.6, // => in units of the x axis + fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill + upFillColor: '#00A8F0',// => up sticks fill color + downFillColor: '#CB4B4B',// => down sticks fill color + fillOpacity: 0.5, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill + // TODO Test this barcharts option. + barcharts: false // => draw as barcharts (not standard bars but financial barcharts) + }, + + draw : function (options) { + + var + context = options.context; + + context.save(); + context.lineJoin = 'miter'; + context.lineCap = 'butt'; + // @TODO linewidth not interpreted the right way. + context.lineWidth = options.wickLineWidth || options.lineWidth; + + this.plot(options); + + context.restore(); + }, + + plot : function (options) { + + var + data = options.data, + context = options.context, + xScale = options.xScale, + yScale = options.yScale, + width = options.candleWidth / 2, + shadowSize = options.shadowSize, + lineWidth = options.lineWidth, + wickLineWidth = options.wickLineWidth, + pixelOffset = (wickLineWidth % 2) / 2, + color, + datum, x, y, + open, high, low, close, + left, right, bottom, top, bottom2, top2, + i; + + if (data.length < 1) return; + + for (i = 0; i < data.length; i++) { + datum = data[i]; + x = datum[0]; + open = datum[1]; + high = datum[2]; + low = datum[3]; + close = datum[4]; + left = xScale(x - width); + right = xScale(x + width); + bottom = yScale(low); + top = yScale(high); + bottom2 = yScale(Math.min(open, close)); + top2 = yScale(Math.max(open, close)); + + /* + // TODO skipping + if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max) + continue; + */ + + color = options[open > close ? 'downFillColor' : 'upFillColor']; + + // Fill the candle. + // TODO Test the barcharts option + if (options.fill && !options.barcharts) { + context.fillStyle = 'rgba(0,0,0,0.05)'; + context.fillRect(left + shadowSize, top2 + shadowSize, right - left, bottom2 - top2); + context.save(); + context.globalAlpha = options.fillOpacity; + context.fillStyle = color; + context.fillRect(left, top2 + lineWidth, right - left, bottom2 - top2); + context.restore(); + } + + // Draw candle outline/border, high, low. + if (lineWidth || wickLineWidth) { + + x = Math.floor((left + right) / 2) + pixelOffset; + + context.strokeStyle = color; + context.beginPath(); + + // TODO Again with the bartcharts + if (options.barcharts) { + + context.moveTo(x, Math.floor(top + width)); + context.lineTo(x, Math.floor(bottom + width)); + + y = Math.floor(open + width) + 0.5; + context.moveTo(Math.floor(left) + pixelOffset, y); + context.lineTo(x, y); + + y = Math.floor(close + width) + 0.5; + context.moveTo(Math.floor(right) + pixelOffset, y); + context.lineTo(x, y); + } else { + context.strokeRect(left, top2 + lineWidth, right - left, bottom2 - top2); + + context.moveTo(x, Math.floor(top2 + lineWidth)); + context.lineTo(x, Math.floor(top + lineWidth)); + context.moveTo(x, Math.floor(bottom2 + lineWidth)); + context.lineTo(x, Math.floor(bottom + lineWidth)); + } + + context.closePath(); + context.stroke(); + } + } + }, + extendXRange: function (axis, data, options) { + if (axis.options.max === null) { + axis.max = Math.max(axis.datamax + 0.5, axis.max); + axis.min = Math.min(axis.datamin - 0.5, axis.min); + } + } +}); + +/** Gantt + * Base on data in form [s,y,d] where: + * y - executor or simply y value + * s - task start value + * d - task duration + * **/ +Flotr.addType('gantt', { + options: { + show: false, // => setting to true will show gantt, false will hide + lineWidth: 2, // => in pixels + barWidth: 1, // => in units of the x axis + fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill + fillColor: null, // => fill color + fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill + centered: true // => center the bars to their x axis value + }, + /** + * Draws gantt series in the canvas element. + * @param {Object} series - Series with options.gantt.show = true. + */ + draw: function(series) { + var ctx = this.ctx, + bw = series.gantt.barWidth, + lw = Math.min(series.gantt.lineWidth, bw); + + ctx.save(); + ctx.translate(this.plotOffset.left, this.plotOffset.top); + ctx.lineJoin = 'miter'; + + /** + * @todo linewidth not interpreted the right way. + */ + ctx.lineWidth = lw; + ctx.strokeStyle = series.color; + + ctx.save(); + this.gantt.plotShadows(series, bw, 0, series.gantt.fill); + ctx.restore(); + + if(series.gantt.fill){ + var color = series.gantt.fillColor || series.color; + ctx.fillStyle = this.processColor(color, {opacity: series.gantt.fillOpacity}); + } + + this.gantt.plot(series, bw, 0, series.gantt.fill); + ctx.restore(); + }, + plot: function(series, barWidth, offset, fill){ + var data = series.data; + if(data.length < 1) return; + + var xa = series.xaxis, + ya = series.yaxis, + ctx = this.ctx, i; + + for(i = 0; i < data.length; i++){ + var y = data[i][0], + s = data[i][1], + d = data[i][2], + drawLeft = true, drawTop = true, drawRight = true; + + if (s === null || d === null) continue; + + var left = s, + right = s + d, + bottom = y - (series.gantt.centered ? barWidth/2 : 0), + top = y + barWidth - (series.gantt.centered ? barWidth/2 : 0); + + if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max) + continue; + + if(left < xa.min){ + left = xa.min; + drawLeft = false; + } + + if(right > xa.max){ + right = xa.max; + if (xa.lastSerie != series) + drawTop = false; + } + + if(bottom < ya.min) + bottom = ya.min; + + if(top > ya.max){ + top = ya.max; + if (ya.lastSerie != series) + drawTop = false; + } + + /** + * Fill the bar. + */ + if(fill){ + ctx.beginPath(); + ctx.moveTo(xa.d2p(left), ya.d2p(bottom) + offset); + ctx.lineTo(xa.d2p(left), ya.d2p(top) + offset); + ctx.lineTo(xa.d2p(right), ya.d2p(top) + offset); + ctx.lineTo(xa.d2p(right), ya.d2p(bottom) + offset); + ctx.fill(); + ctx.closePath(); + } + + /** + * Draw bar outline/border. + */ + if(series.gantt.lineWidth && (drawLeft || drawRight || drawTop)){ + ctx.beginPath(); + ctx.moveTo(xa.d2p(left), ya.d2p(bottom) + offset); + + ctx[drawLeft ?'lineTo':'moveTo'](xa.d2p(left), ya.d2p(top) + offset); + ctx[drawTop ?'lineTo':'moveTo'](xa.d2p(right), ya.d2p(top) + offset); + ctx[drawRight?'lineTo':'moveTo'](xa.d2p(right), ya.d2p(bottom) + offset); + + ctx.stroke(); + ctx.closePath(); + } + } + }, + plotShadows: function(series, barWidth, offset){ + var data = series.data; + if(data.length < 1) return; + + var i, y, s, d, + xa = series.xaxis, + ya = series.yaxis, + ctx = this.ctx, + sw = this.options.shadowSize; + + for(i = 0; i < data.length; i++){ + y = data[i][0]; + s = data[i][1]; + d = data[i][2]; + + if (s === null || d === null) continue; + + var left = s, + right = s + d, + bottom = y - (series.gantt.centered ? barWidth/2 : 0), + top = y + barWidth - (series.gantt.centered ? barWidth/2 : 0); + + if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max) + continue; + + if(left < xa.min) left = xa.min; + if(right > xa.max) right = xa.max; + if(bottom < ya.min) bottom = ya.min; + if(top > ya.max) top = ya.max; + + var width = xa.d2p(right)-xa.d2p(left)-((xa.d2p(right)+sw <= this.plotWidth) ? 0 : sw); + var height = ya.d2p(bottom)-ya.d2p(top)-((ya.d2p(bottom)+sw <= this.plotHeight) ? 0 : sw ); + + ctx.fillStyle = 'rgba(0,0,0,0.05)'; + ctx.fillRect(Math.min(xa.d2p(left)+sw, this.plotWidth), Math.min(ya.d2p(top)+sw, this.plotHeight), width, height); + } + }, + extendXRange: function(axis) { + if(axis.options.max === null){ + var newmin = axis.min, + newmax = axis.max, + i, j, x, s, g, + stackedSumsPos = {}, + stackedSumsNeg = {}, + lastSerie = null; + + for(i = 0; i < this.series.length; ++i){ + s = this.series[i]; + g = s.gantt; + + if(g.show && s.xaxis == axis) { + for (j = 0; j < s.data.length; j++) { + if (g.show) { + y = s.data[j][0]+''; + stackedSumsPos[y] = Math.max((stackedSumsPos[y] || 0), s.data[j][1]+s.data[j][2]); + lastSerie = s; + } + } + for (j in stackedSumsPos) { + newmax = Math.max(stackedSumsPos[j], newmax); + } + } + } + axis.lastSerie = lastSerie; + axis.max = newmax; + axis.min = newmin; + } + }, + extendYRange: function(axis){ + if(axis.options.max === null){ + var newmax = Number.MIN_VALUE, + newmin = Number.MAX_VALUE, + i, j, s, g, + stackedSumsPos = {}, + stackedSumsNeg = {}, + lastSerie = null; + + for(i = 0; i < this.series.length; ++i){ + s = this.series[i]; + g = s.gantt; + + if (g.show && !s.hide && s.yaxis == axis) { + var datamax = Number.MIN_VALUE, datamin = Number.MAX_VALUE; + for(j=0; j < s.data.length; j++){ + datamax = Math.max(datamax,s.data[j][0]); + datamin = Math.min(datamin,s.data[j][0]); + } + + if (g.centered) { + newmax = Math.max(datamax + 0.5, newmax); + newmin = Math.min(datamin - 0.5, newmin); + } + else { + newmax = Math.max(datamax + 1, newmax); + newmin = Math.min(datamin, newmin); + } + // For normal horizontal bars + if (g.barWidth + datamax > newmax){ + newmax = axis.max + g.barWidth; + } + } + } + axis.lastSerie = lastSerie; + axis.max = newmax; + axis.min = newmin; + axis.tickSize = Flotr.getTickSize(axis.options.noTicks, newmin, newmax, axis.options.tickDecimals); + } + } +}); + +/** Markers **/ +/** + * Formats the marker labels. + * @param {Object} obj - Marker value Object {x:..,y:..} + * @return {String} Formatted marker string + */ +(function () { + +Flotr.defaultMarkerFormatter = function(obj){ + return (Math.round(obj.y*100)/100)+''; +}; + +Flotr.addType('markers', { + options: { + show: false, // => setting to true will show markers, false will hide + lineWidth: 1, // => line width of the rectangle around the marker + color: '#000000', // => text color + fill: false, // => fill or not the marekers' rectangles + fillColor: "#FFFFFF", // => fill color + fillOpacity: 0.4, // => fill opacity + stroke: false, // => draw the rectangle around the markers + position: 'ct', // => the markers position (vertical align: b, m, t, horizontal align: l, c, r) + verticalMargin: 0, // => the margin between the point and the text. + labelFormatter: Flotr.defaultMarkerFormatter, + fontSize: Flotr.defaultOptions.fontSize, + stacked: false, // => true if markers should be stacked + stackingType: 'b', // => define staching behavior, (b- bars like, a - area like) (see Issue 125 for details) + horizontal: false // => true if markers should be horizontal (For now only in a case on horizontal stacked bars, stacks should be calculated horizontaly) + }, + + // TODO test stacked markers. + stack : { + positive : [], + negative : [], + values : [] + }, + + draw : function (options) { + + var + data = options.data, + context = options.context, + stack = options.stacked ? options.stack : false, + stackType = options.stackingType, + stackOffsetNeg, + stackOffsetPos, + stackOffset, + i, x, y, label; + + context.save(); + context.lineJoin = 'round'; + context.lineWidth = options.lineWidth; + context.strokeStyle = 'rgba(0,0,0,0.5)'; + context.fillStyle = options.fillStyle; + + function stackPos (a, b) { + stackOffsetPos = stack.negative[a] || 0; + stackOffsetNeg = stack.positive[a] || 0; + if (b > 0) { + stack.positive[a] = stackOffsetPos + b; + return stackOffsetPos + b; + } else { + stack.negative[a] = stackOffsetNeg + b; + return stackOffsetNeg + b; + } + } + + for (i = 0; i < data.length; ++i) { + + x = data[i][0]; + y = data[i][1]; + + if (stack) { + if (stackType == 'b') { + if (options.horizontal) y = stackPos(y, x); + else x = stackPos(x, y); + } else if (stackType == 'a') { + stackOffset = stack.values[x] || 0; + stack.values[x] = stackOffset + y; + y = stackOffset + y; + } + } + + label = options.labelFormatter({x: x, y: y, index: i, data : data}); + this.plot(options.xScale(x), options.yScale(y), label, options); + } + context.restore(); + }, + plot: function(x, y, label, options) { + var context = options.context; + if (isImage(label) && !label.complete) { + throw 'Marker image not loaded.'; + } else { + this._plot(x, y, label, options); + } + }, + + _plot: function(x, y, label, options) { + var context = options.context, + margin = 2, + left = x, + top = y, + dim; + + if (isImage(label)) + dim = {height : label.height, width: label.width}; + else + dim = options.text.canvas(label); + + dim.width = Math.floor(dim.width+margin*2); + dim.height = Math.floor(dim.height+margin*2); + + if (options.position.indexOf('c') != -1) left -= dim.width/2 + margin; + else if (options.position.indexOf('l') != -1) left -= dim.width; + + if (options.position.indexOf('m') != -1) top -= dim.height/2 + margin; + else if (options.position.indexOf('t') != -1) top -= dim.height + options.verticalMargin; + else top += options.verticalMargin; + + left = Math.floor(left)+0.5; + top = Math.floor(top)+0.5; + + if(options.fill) + context.fillRect(left, top, dim.width, dim.height); + + if(options.stroke) + context.strokeRect(left, top, dim.width, dim.height); + + if (isImage(label)) + context.drawImage(label, left+margin, top+margin); + else + Flotr.drawText(context, label, left+margin, top+margin, {textBaseline: 'top', textAlign: 'left', size: options.fontSize, color: options.color}); + } +}); + +function isImage (i) { + return typeof i === 'object' && i.constructor && (Image ? true : i.constructor === Image); +} + +})(); + +/** + * Pie + * + * Formats the pies labels. + * @param {Object} slice - Slice object + * @return {String} Formatted pie label string + */ +(function () { + +var + _ = Flotr._; + +Flotr.defaultPieLabelFormatter = function (total, value) { + return (100 * value / total).toFixed(2)+'%'; +}; + +Flotr.addType('pie', { + options: { + show: false, // => setting to true will show bars, false will hide + lineWidth: 1, // => in pixels + fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill + fillColor: null, // => fill color + fillOpacity: 0.6, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill + explode: 6, // => the number of pixels the splices will be far from the center + sizeRatio: 0.6, // => the size ratio of the pie relative to the plot + startAngle: Math.PI/4, // => the first slice start angle + labelFormatter: Flotr.defaultPieLabelFormatter, + pie3D: false, // => whether to draw the pie in 3 dimenstions or not (ineffective) + pie3DviewAngle: (Math.PI/2 * 0.8), + pie3DspliceThickness: 20, + epsilon: 0.1 // => how close do you have to get to hit empty slice + }, + + draw : function (options) { + + // TODO 3D charts what? + + var + data = options.data, + context = options.context, + canvas = context.canvas, + lineWidth = options.lineWidth, + shadowSize = options.shadowSize, + sizeRatio = options.sizeRatio, + height = options.height, + width = options.width, + explode = options.explode, + color = options.color, + fill = options.fill, + fillStyle = options.fillStyle, + radius = Math.min(canvas.width, canvas.height) * sizeRatio / 2, + value = data[0][1], + html = [], + vScale = 1,//Math.cos(series.pie.viewAngle); + measure = Math.PI * 2 * value / this.total, + startAngle = this.startAngle || (2 * Math.PI * options.startAngle), // TODO: this initial startAngle is already in radians (fixing will be test-unstable) + endAngle = startAngle + measure, + bisection = startAngle + measure / 2, + label = options.labelFormatter(this.total, value), + //plotTickness = Math.sin(series.pie.viewAngle)*series.pie.spliceThickness / vScale; + explodeCoeff = explode + radius + 4, + distX = Math.cos(bisection) * explodeCoeff, + distY = Math.sin(bisection) * explodeCoeff, + textAlign = distX < 0 ? 'right' : 'left', + textBaseline = distY > 0 ? 'top' : 'bottom', + style, + x, y; + + context.save(); + context.translate(width / 2, height / 2); + context.scale(1, vScale); + + x = Math.cos(bisection) * explode; + y = Math.sin(bisection) * explode; + + // Shadows + if (shadowSize > 0) { + this.plotSlice(x + shadowSize, y + shadowSize, radius, startAngle, endAngle, context); + if (fill) { + context.fillStyle = 'rgba(0,0,0,0.1)'; + context.fill(); + } + } + + this.plotSlice(x, y, radius, startAngle, endAngle, context); + if (fill) { + context.fillStyle = fillStyle; + context.fill(); + } + context.lineWidth = lineWidth; + context.strokeStyle = color; + context.stroke(); + + style = { + size : options.fontSize * 1.2, + color : options.fontColor, + weight : 1.5 + }; + + if (label) { + if (options.htmlText || !options.textEnabled) { + divStyle = 'position:absolute;' + textBaseline + ':' + (height / 2 + (textBaseline === 'top' ? distY : -distY)) + 'px;'; + divStyle += textAlign + ':' + (width / 2 + (textAlign === 'right' ? -distX : distX)) + 'px;'; + html.push('
', label, '
'); + } + else { + style.textAlign = textAlign; + style.textBaseline = textBaseline; + Flotr.drawText(context, label, distX, distY, style); + } + } + + if (options.htmlText || !options.textEnabled) { + var div = Flotr.DOM.node('
'); + Flotr.DOM.insert(div, html.join('')); + Flotr.DOM.insert(options.element, div); + } + + context.restore(); + + // New start angle + this.startAngle = endAngle; + this.slices = this.slices || []; + this.slices.push({ + radius : Math.min(canvas.width, canvas.height) * sizeRatio / 2, + x : x, + y : y, + explode : explode, + start : startAngle, + end : endAngle + }); + }, + plotSlice : function (x, y, radius, startAngle, endAngle, context) { + context.beginPath(); + context.moveTo(x, y); + context.arc(x, y, radius, startAngle, endAngle, false); + context.lineTo(x, y); + context.closePath(); + }, + hit : function (options) { + + var + data = options.data[0], + args = options.args, + index = options.index, + mouse = args[0], + n = args[1], + slice = this.slices[index], + x = mouse.relX - options.width / 2, + y = mouse.relY - options.height / 2, + r = Math.sqrt(x * x + y * y), + theta = Math.atan(y / x), + circle = Math.PI * 2, + explode = slice.explode || options.explode, + start = slice.start % circle, + end = slice.end % circle, + epsilon = options.epsilon; + + if (x < 0) { + theta += Math.PI; + } else if (x > 0 && y < 0) { + theta += circle; + } + + if (r < slice.radius + explode && r > explode) { + if ( + (theta > start && theta < end) || // Normal Slice + (start > end && (theta < end || theta > start)) || // First slice + // TODO: Document the two cases at the end: + (start === end && ((slice.start === slice.end && Math.abs(theta - start) < epsilon) || (slice.start !== slice.end && Math.abs(theta-start) > epsilon))) + ) { + + // TODO Decouple this from hit plugin (chart shouldn't know what n means) + n.x = data[0]; + n.y = data[1]; + n.sAngle = start; + n.eAngle = end; + n.index = 0; + n.seriesIndex = index; + n.fraction = data[1] / this.total; + } + } + }, + drawHit: function (options) { + var + context = options.context, + slice = this.slices[options.args.seriesIndex]; + + context.save(); + context.translate(options.width / 2, options.height / 2); + this.plotSlice(slice.x, slice.y, slice.radius, slice.start, slice.end, context); + context.stroke(); + context.restore(); + }, + clearHit : function (options) { + var + context = options.context, + slice = this.slices[options.args.seriesIndex], + padding = 2 * options.lineWidth, + radius = slice.radius + padding; + + context.save(); + context.translate(options.width / 2, options.height / 2); + context.clearRect( + slice.x - radius, + slice.y - radius, + 2 * radius + padding, + 2 * radius + padding + ); + context.restore(); + }, + extendYRange : function (axis, data) { + this.total = (this.total || 0) + data[0][1]; + } +}); +})(); + +/** Points **/ +Flotr.addType('points', { + options: { + show: false, // => setting to true will show points, false will hide + radius: 3, // => point radius (pixels) + lineWidth: 2, // => line width in pixels + fill: true, // => true to fill the points with a color, false for (transparent) no fill + fillColor: '#FFFFFF', // => fill color. Null to use series color. + fillOpacity: 1, // => opacity of color inside the points + hitRadius: null // => override for points hit radius + }, + + draw : function (options) { + var + context = options.context, + lineWidth = options.lineWidth, + shadowSize = options.shadowSize; + + context.save(); + + if (shadowSize > 0) { + context.lineWidth = shadowSize / 2; + + context.strokeStyle = 'rgba(0,0,0,0.1)'; + this.plot(options, shadowSize / 2 + context.lineWidth / 2); + + context.strokeStyle = 'rgba(0,0,0,0.2)'; + this.plot(options, context.lineWidth / 2); + } + + context.lineWidth = options.lineWidth; + context.strokeStyle = options.color; + if (options.fill) context.fillStyle = options.fillStyle; + + this.plot(options); + context.restore(); + }, + + plot : function (options, offset) { + var + data = options.data, + context = options.context, + xScale = options.xScale, + yScale = options.yScale, + i, x, y; + + for (i = data.length - 1; i > -1; --i) { + y = data[i][1]; + if (y === null) continue; + + x = xScale(data[i][0]); + y = yScale(y); + + if (x < 0 || x > options.width || y < 0 || y > options.height) continue; + + context.beginPath(); + if (offset) { + context.arc(x, y + offset, options.radius, 0, Math.PI, false); + } else { + context.arc(x, y, options.radius, 0, 2 * Math.PI, true); + if (options.fill) context.fill(); + } + context.stroke(); + context.closePath(); + } + } +}); + +/** Radar **/ +Flotr.addType('radar', { + options: { + show: false, // => setting to true will show radar chart, false will hide + lineWidth: 2, // => line width in pixels + fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill + fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill + radiusRatio: 0.90 // => ratio of the radar, against the plot size + }, + draw : function (options) { + var + context = options.context, + shadowSize = options.shadowSize; + + context.save(); + context.translate(options.width / 2, options.height / 2); + context.lineWidth = options.lineWidth; + + // Shadow + context.fillStyle = 'rgba(0,0,0,0.05)'; + context.strokeStyle = 'rgba(0,0,0,0.05)'; + this.plot(options, shadowSize / 2); + context.strokeStyle = 'rgba(0,0,0,0.1)'; + this.plot(options, shadowSize / 4); + + // Chart + context.strokeStyle = options.color; + context.fillStyle = options.fillStyle; + this.plot(options); + + context.restore(); + }, + plot : function (options, offset) { + var + data = options.data, + context = options.context, + radius = Math.min(options.height, options.width) * options.radiusRatio / 2, + step = 2 * Math.PI / data.length, + angle = -Math.PI / 2, + i, ratio; + + offset = offset || 0; + + context.beginPath(); + for (i = 0; i < data.length; ++i) { + ratio = data[i][1] / this.max; + + context[i === 0 ? 'moveTo' : 'lineTo']( + Math.cos(i * step + angle) * radius * ratio + offset, + Math.sin(i * step + angle) * radius * ratio + offset + ); + } + context.closePath(); + if (options.fill) context.fill(); + context.stroke(); + }, + extendYRange : function (axis, data) { + this.max = Math.max(axis.max, this.max || -Number.MAX_VALUE); + } +}); + +Flotr.addType('timeline', { + options: { + show: false, + lineWidth: 1, + barWidth: 0.2, + fill: true, + fillColor: null, + fillOpacity: 0.4, + centered: true + }, + + draw : function (options) { + + var + context = options.context; + + context.save(); + context.lineJoin = 'miter'; + context.lineWidth = options.lineWidth; + context.strokeStyle = options.color; + context.fillStyle = options.fillStyle; + + this.plot(options); + + context.restore(); + }, + + plot : function (options) { + + var + data = options.data, + context = options.context, + xScale = options.xScale, + yScale = options.yScale, + barWidth = options.barWidth, + lineWidth = options.lineWidth, + i; + + Flotr._.each(data, function (timeline) { + + var + x = timeline[0], + y = timeline[1], + w = timeline[2], + h = barWidth, + + xt = Math.ceil(xScale(x)), + wt = Math.ceil(xScale(x + w)) - xt, + yt = Math.round(yScale(y)), + ht = Math.round(yScale(y - h)) - yt, + + x0 = xt - lineWidth / 2, + y0 = Math.round(yt - ht / 2) - lineWidth / 2; + + context.strokeRect(x0, y0, wt, ht); + context.fillRect(x0, y0, wt, ht); + + }); + }, + + extendRange : function (series) { + + var + data = series.data, + xa = series.xaxis, + ya = series.yaxis, + w = series.timeline.barWidth; + + if (xa.options.min === null) + xa.min = xa.datamin - w / 2; + + if (xa.options.max === null) { + + var + max = xa.max; + + Flotr._.each(data, function (timeline) { + max = Math.max(max, timeline[0] + timeline[2]); + }, this); + + xa.max = max + w / 2; + } + + if (ya.options.min === null) + ya.min = ya.datamin - w; + if (ya.options.min === null) + ya.max = ya.datamax + w; + } + +}); + +(function () { + +var D = Flotr.DOM; + +Flotr.addPlugin('crosshair', { + options: { + mode: null, // => one of null, 'x', 'y' or 'xy' + color: '#FF0000', // => crosshair color + hideCursor: true // => hide the cursor when the crosshair is shown + }, + callbacks: { + 'flotr:mousemove': function(e, pos) { + if (this.options.crosshair.mode) { + this.crosshair.clearCrosshair(); + this.crosshair.drawCrosshair(pos); + } + } + }, + /** + * Draws the selection box. + */ + drawCrosshair: function(pos) { + var octx = this.octx, + options = this.options.crosshair, + plotOffset = this.plotOffset, + x = plotOffset.left + Math.round(pos.relX) + 0.5, + y = plotOffset.top + Math.round(pos.relY) + 0.5; + + if (pos.relX < 0 || pos.relY < 0 || pos.relX > this.plotWidth || pos.relY > this.plotHeight) { + this.el.style.cursor = null; + D.removeClass(this.el, 'flotr-crosshair'); + return; + } + + if (options.hideCursor) { + this.el.style.cursor = 'none'; + D.addClass(this.el, 'flotr-crosshair'); + } + + octx.save(); + octx.strokeStyle = options.color; + octx.lineWidth = 1; + octx.beginPath(); + + if (options.mode.indexOf('x') != -1) { + octx.moveTo(x, plotOffset.top); + octx.lineTo(x, plotOffset.top + this.plotHeight); + } + + if (options.mode.indexOf('y') != -1) { + octx.moveTo(plotOffset.left, y); + octx.lineTo(plotOffset.left + this.plotWidth, y); + } + + octx.stroke(); + octx.restore(); + }, + /** + * Removes the selection box from the overlay canvas. + */ + clearCrosshair: function() { + + var + plotOffset = this.plotOffset, + position = this.lastMousePos, + context = this.octx; + + if (position) { + context.clearRect( + Math.round(position.relX) + plotOffset.left, + plotOffset.top, + 1, + this.plotHeight + 1 + ); + context.clearRect( + plotOffset.left, + Math.round(position.relY) + plotOffset.top, + this.plotWidth + 1, + 1 + ); + } + } +}); +})(); + +(function() { + +var + D = Flotr.DOM, + _ = Flotr._; + +function getImage (type, canvas, width, height) { + + // TODO add scaling for w / h + var + mime = 'image/'+type, + data = canvas.toDataURL(mime), + image = new Image(); + image.src = data; + return image; +} + +Flotr.addPlugin('download', { + + saveImage: function (type, width, height, replaceCanvas) { + var image = null; + if (Flotr.isIE && Flotr.isIE < 9) { + image = ''+this.canvas.firstChild.innerHTML+''; + return window.open().document.write(image); + } + + if (type !== 'jpeg' && type !== 'png') return; + + image = getImage(type, this.canvas, width, height); + + if (_.isElement(image) && replaceCanvas) { + this.download.restoreCanvas(); + D.hide(this.canvas); + D.hide(this.overlay); + D.setStyles({position: 'absolute'}); + D.insert(this.el, image); + this.saveImageElement = image; + } else { + return window.open(image.src); + } + }, + + restoreCanvas: function() { + D.show(this.canvas); + D.show(this.overlay); + if (this.saveImageElement) this.el.removeChild(this.saveImageElement); + this.saveImageElement = null; + } +}); + +})(); + +(function () { + +var E = Flotr.EventAdapter, + _ = Flotr._; + +Flotr.addPlugin('graphGrid', { + + callbacks: { + 'flotr:beforedraw' : function () { + this.graphGrid.drawGrid(); + }, + 'flotr:afterdraw' : function () { + this.graphGrid.drawOutline(); + } + }, + + drawGrid: function(){ + + var + ctx = this.ctx, + options = this.options, + grid = options.grid, + verticalLines = grid.verticalLines, + horizontalLines = grid.horizontalLines, + minorVerticalLines = grid.minorVerticalLines, + minorHorizontalLines = grid.minorHorizontalLines, + plotHeight = this.plotHeight, + plotWidth = this.plotWidth, + a, v, i, j; + + if(verticalLines || minorVerticalLines || + horizontalLines || minorHorizontalLines){ + E.fire(this.el, 'flotr:beforegrid', [this.axes.x, this.axes.y, options, this]); + } + ctx.save(); + ctx.lineWidth = 1; + ctx.strokeStyle = grid.tickColor; + + function circularHorizontalTicks (ticks) { + for(i = 0; i < ticks.length; ++i){ + var ratio = ticks[i].v / a.max; + for(j = 0; j <= sides; ++j){ + ctx[j === 0 ? 'moveTo' : 'lineTo']( + Math.cos(j*coeff+angle)*radius*ratio, + Math.sin(j*coeff+angle)*radius*ratio + ); + } + } + } + function drawGridLines (ticks, callback) { + _.each(_.pluck(ticks, 'v'), function(v){ + // Don't show lines on upper and lower bounds. + if ((v <= a.min || v >= a.max) || + (v == a.min || v == a.max) && grid.outlineWidth) + return; + callback(Math.floor(a.d2p(v)) + ctx.lineWidth/2); + }); + } + function drawVerticalLines (x) { + ctx.moveTo(x, 0); + ctx.lineTo(x, plotHeight); + } + function drawHorizontalLines (y) { + ctx.moveTo(0, y); + ctx.lineTo(plotWidth, y); + } + + if (grid.circular) { + ctx.translate(this.plotOffset.left+plotWidth/2, this.plotOffset.top+plotHeight/2); + var radius = Math.min(plotHeight, plotWidth)*options.radar.radiusRatio/2, + sides = this.axes.x.ticks.length, + coeff = 2*(Math.PI/sides), + angle = -Math.PI/2; + + // Draw grid lines in vertical direction. + ctx.beginPath(); + + a = this.axes.y; + + if(horizontalLines){ + circularHorizontalTicks(a.ticks); + } + if(minorHorizontalLines){ + circularHorizontalTicks(a.minorTicks); + } + + if(verticalLines){ + _.times(sides, function(i){ + ctx.moveTo(0, 0); + ctx.lineTo(Math.cos(i*coeff+angle)*radius, Math.sin(i*coeff+angle)*radius); + }); + } + ctx.stroke(); + } + else { + ctx.translate(this.plotOffset.left, this.plotOffset.top); + + // Draw grid background, if present in options. + if(grid.backgroundColor){ + ctx.fillStyle = this.processColor(grid.backgroundColor, {x1: 0, y1: 0, x2: plotWidth, y2: plotHeight}); + ctx.fillRect(0, 0, plotWidth, plotHeight); + } + + ctx.beginPath(); + + a = this.axes.x; + if (verticalLines) drawGridLines(a.ticks, drawVerticalLines); + if (minorVerticalLines) drawGridLines(a.minorTicks, drawVerticalLines); + + a = this.axes.y; + if (horizontalLines) drawGridLines(a.ticks, drawHorizontalLines); + if (minorHorizontalLines) drawGridLines(a.minorTicks, drawHorizontalLines); + + ctx.stroke(); + } + + ctx.restore(); + if(verticalLines || minorVerticalLines || + horizontalLines || minorHorizontalLines){ + E.fire(this.el, 'flotr:aftergrid', [this.axes.x, this.axes.y, options, this]); + } + }, + + drawOutline: function(){ + var + that = this, + options = that.options, + grid = options.grid, + outline = grid.outline, + ctx = that.ctx, + backgroundImage = grid.backgroundImage, + plotOffset = that.plotOffset, + leftOffset = plotOffset.left, + topOffset = plotOffset.top, + plotWidth = that.plotWidth, + plotHeight = that.plotHeight, + v, img, src, left, top, globalAlpha; + + if (!grid.outlineWidth) return; + + ctx.save(); + + if (grid.circular) { + ctx.translate(leftOffset + plotWidth / 2, topOffset + plotHeight / 2); + var radius = Math.min(plotHeight, plotWidth) * options.radar.radiusRatio / 2, + sides = this.axes.x.ticks.length, + coeff = 2*(Math.PI/sides), + angle = -Math.PI/2; + + // Draw axis/grid border. + ctx.beginPath(); + ctx.lineWidth = grid.outlineWidth; + ctx.strokeStyle = grid.color; + ctx.lineJoin = 'round'; + + for(i = 0; i <= sides; ++i){ + ctx[i === 0 ? 'moveTo' : 'lineTo'](Math.cos(i*coeff+angle)*radius, Math.sin(i*coeff+angle)*radius); + } + //ctx.arc(0, 0, radius, 0, Math.PI*2, true); + + ctx.stroke(); + } + else { + ctx.translate(leftOffset, topOffset); + + // Draw axis/grid border. + var lw = grid.outlineWidth, + orig = 0.5-lw+((lw+1)%2/2), + lineTo = 'lineTo', + moveTo = 'moveTo'; + ctx.lineWidth = lw; + ctx.strokeStyle = grid.color; + ctx.lineJoin = 'miter'; + ctx.beginPath(); + ctx.moveTo(orig, orig); + plotWidth = plotWidth - (lw / 2) % 1; + plotHeight = plotHeight + lw / 2; + ctx[outline.indexOf('n') !== -1 ? lineTo : moveTo](plotWidth, orig); + ctx[outline.indexOf('e') !== -1 ? lineTo : moveTo](plotWidth, plotHeight); + ctx[outline.indexOf('s') !== -1 ? lineTo : moveTo](orig, plotHeight); + ctx[outline.indexOf('w') !== -1 ? lineTo : moveTo](orig, orig); + ctx.stroke(); + ctx.closePath(); + } + + ctx.restore(); + + if (backgroundImage) { + + src = backgroundImage.src || backgroundImage; + left = (parseInt(backgroundImage.left, 10) || 0) + plotOffset.left; + top = (parseInt(backgroundImage.top, 10) || 0) + plotOffset.top; + img = new Image(); + + img.onload = function() { + ctx.save(); + if (backgroundImage.alpha) ctx.globalAlpha = backgroundImage.alpha; + ctx.globalCompositeOperation = 'destination-over'; + ctx.drawImage(img, 0, 0, img.width, img.height, left, top, plotWidth, plotHeight); + ctx.restore(); + }; + + img.src = src; + } + } +}); + +})(); + +(function () { + +var + D = Flotr.DOM, + _ = Flotr._, + flotr = Flotr, + S_MOUSETRACK = 'opacity:0.7;background-color:#000;color:#fff;display:none;position:absolute;padding:2px 8px;-moz-border-radius:4px;border-radius:4px;white-space:nowrap;'; + +Flotr.addPlugin('hit', { + callbacks: { + 'flotr:mousemove': function(e, pos) { + this.hit.track(pos); + }, + 'flotr:click': function(pos) { + 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 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.addPlugin('labels', { + + callbacks : { + 'flotr:afterdraw' : function () { + this.labels.draw(); + } + }, + + draw: function(){ + // Construct fixed width label boxes, which can be styled easily. + var + axis, tick, left, top, xBoxWidth, + radius, sides, coeff, angle, + div, i, html = '', + noLabels = 0, + options = this.options, + ctx = this.ctx, + a = this.axes, + style = { size: options.fontSize }; + + for (i = 0; i < a.x.ticks.length; ++i){ + if (a.x.ticks[i].label) { ++noLabels; } + } + xBoxWidth = this.plotWidth / noLabels; + + if (options.grid.circular) { + ctx.save(); + ctx.translate(this.plotOffset.left + this.plotWidth / 2, + this.plotOffset.top + this.plotHeight / 2); + + radius = this.plotHeight * options.radar.radiusRatio / 2 + options.fontSize; + sides = this.axes.x.ticks.length; + coeff = 2 * (Math.PI / sides); + angle = -Math.PI / 2; + + drawLabelCircular(this, a.x, false); + drawLabelCircular(this, a.x, true); + drawLabelCircular(this, a.y, false); + drawLabelCircular(this, a.y, true); + ctx.restore(); + } + + if (!options.HtmlText && this.textEnabled) { + drawLabelNoHtmlText(this, a.x, 'center', 'top'); + drawLabelNoHtmlText(this, a.x2, 'center', 'bottom'); + drawLabelNoHtmlText(this, a.y, 'right', 'middle'); + drawLabelNoHtmlText(this, a.y2, 'left', 'middle'); + + } else if (( + a.x.options.showLabels || + a.x2.options.showLabels || + a.y.options.showLabels || + a.y2.options.showLabels) && + !options.grid.circular + ) { + + html = ''; + + drawLabelHtml(this, a.x); + drawLabelHtml(this, a.x2); + drawLabelHtml(this, a.y); + drawLabelHtml(this, a.y2); + + ctx.stroke(); + ctx.restore(); + div = D.create('div'); + D.setStyles(div, { + fontSize: 'smaller', + color: options.grid.color + }); + div.className = 'flotr-labels'; + D.insert(this.el, div); + D.insert(div, html); + } + + function drawLabelCircular (graph, axis, minorTicks) { + var + ticks = minorTicks ? axis.minorTicks : axis.ticks, + isX = axis.orientation === 1, + isFirst = axis.n === 1, + style, offset; + + style = { + color : axis.options.color || options.grid.color, + angle : Flotr.toRad(axis.options.labelsAngle), + textBaseline : 'middle' + }; + + for (i = 0; i < ticks.length && + (minorTicks ? axis.options.showMinorLabels : axis.options.showLabels); ++i){ + tick = ticks[i]; + tick.label += ''; + if (!tick.label || !tick.label.length) { continue; } + + x = Math.cos(i * coeff + angle) * radius; + y = Math.sin(i * coeff + angle) * radius; + + style.textAlign = isX ? (Math.abs(x) < 0.1 ? 'center' : (x < 0 ? 'right' : 'left')) : 'left'; + + Flotr.drawText( + ctx, tick.label, + isX ? x : 3, + isX ? y : -(axis.ticks[i].v / axis.max) * (radius - options.fontSize), + style + ); + } + } + + function drawLabelNoHtmlText (graph, axis, textAlign, textBaseline) { + var + isX = axis.orientation === 1, + isFirst = axis.n === 1, + style, offset; + + style = { + color : axis.options.color || options.grid.color, + textAlign : textAlign, + textBaseline : textBaseline, + angle : Flotr.toRad(axis.options.labelsAngle) + }; + style = Flotr.getBestTextAlign(style.angle, style); + + for (i = 0; i < axis.ticks.length && continueShowingLabels(axis); ++i) { + + tick = axis.ticks[i]; + if (!tick.label || !tick.label.length) { continue; } + + offset = axis.d2p(tick.v); + if (offset < 0 || + offset > (isX ? graph.plotWidth : graph.plotHeight)) { continue; } + + Flotr.drawText( + ctx, tick.label, + leftOffset(graph, isX, isFirst, offset), + topOffset(graph, isX, isFirst, offset), + style + ); + + // Only draw on axis y2 + if (!isX && !isFirst) { + ctx.save(); + ctx.strokeStyle = style.color; + ctx.beginPath(); + ctx.moveTo(graph.plotOffset.left + graph.plotWidth - 8, graph.plotOffset.top + axis.d2p(tick.v)); + ctx.lineTo(graph.plotOffset.left + graph.plotWidth, graph.plotOffset.top + axis.d2p(tick.v)); + ctx.stroke(); + ctx.restore(); + } + } + + function continueShowingLabels (axis) { + return axis.options.showLabels && axis.used; + } + function leftOffset (graph, isX, isFirst, offset) { + return graph.plotOffset.left + + (isX ? offset : + (isFirst ? + -options.grid.labelMargin : + options.grid.labelMargin + graph.plotWidth)); + } + function topOffset (graph, isX, isFirst, offset) { + return graph.plotOffset.top + + (isX ? options.grid.labelMargin : offset) + + ((isX && isFirst) ? graph.plotHeight : 0); + } + } + + function drawLabelHtml (graph, axis) { + var + isX = axis.orientation === 1, + isFirst = axis.n === 1, + name = '', + left, style, top, + offset = graph.plotOffset; + + if (!isX && !isFirst) { + ctx.save(); + ctx.strokeStyle = axis.options.color || options.grid.color; + ctx.beginPath(); + } + + if (axis.options.showLabels && (isFirst ? true : axis.used)) { + for (i = 0; i < axis.ticks.length; ++i) { + tick = axis.ticks[i]; + if (!tick.label || !tick.label.length || + ((isX ? offset.left : offset.top) + axis.d2p(tick.v) < 0) || + ((isX ? offset.left : offset.top) + axis.d2p(tick.v) > (isX ? graph.canvasWidth : graph.canvasHeight))) { + continue; + } + top = offset.top + + (isX ? + ((isFirst ? 1 : -1 ) * (graph.plotHeight + options.grid.labelMargin)) : + axis.d2p(tick.v) - axis.maxLabel.height / 2); + left = isX ? (offset.left + axis.d2p(tick.v) - xBoxWidth / 2) : 0; + + name = ''; + if (i === 0) { + name = ' first'; + } else if (i === axis.ticks.length - 1) { + name = ' last'; + } + name += isX ? ' flotr-grid-label-x' : ' flotr-grid-label-y'; + + html += [ + '
' + tick.label + '
' + ].join(' '); + + if (!isX && !isFirst) { + ctx.moveTo(offset.left + graph.plotWidth - 8, offset.top + axis.d2p(tick.v)); + ctx.lineTo(offset.left + graph.plotWidth, offset.top + axis.d2p(tick.v)); + } + } + } + } + } + +}); +})(); + +(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(); + }, + 'flotr:destroy': function() { + var markup = this.legend.markup; + if (markup) { + this.legend.markup = null; + D.remove(markup); + } + } + }, + /** + * 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){ + table = D.node(table); + this.legend.markup = table; + D.insert(legend.container, table); + } + else { + var styles = {position: 'absolute', 'zIndex': '2', 'border' : '1px solid ' + legend.labelBoxBorderColor}; + + if(p.charAt(0) == 'n') { styles.top = (m + plotOffset.top) + 'px'; styles.bottom = 'auto'; } + else if(p.charAt(0) == 'c') { styles.top = (m + (this.plotHeight - legendHeight) / 2) + 'px'; styles.bottom = 'auto'; } + else if(p.charAt(0) == 's') { styles.bottom = (m + plotOffset.bottom) + 'px'; styles.top = 'auto'; } + if(p.charAt(1) == 'e') { styles.right = (m + plotOffset.right) + 'px'; styles.left = 'auto'; } + else if(p.charAt(1) == 'w') { styles.left = (m + plotOffset.left) + 'px'; styles.right = 'auto'; } + + var div = D.create('div'), size; + div.className = 'flotr-legend'; + D.setStyles(div, styles); + D.insert(div, table); + D.insert(this.el, div); + + if (!opacity) return; + + var c = legend.backgroundColor || options.grid.backgroundColor || '#ffffff'; + + _.extend(styles, D.size(div), { + 'backgroundColor': c, + 'zIndex' : '', + 'border' : '' + }); + styles.width += 'px'; + styles.height += 'px'; + + // Put in the transparent background separately to avoid blended labels and + div = D.create('div'); + div.className = 'flotr-legend-bg'; + D.setStyles(div, styles); + D.opacity(div, opacity); + D.insert(div, ' '); + D.insert(this.el, div); + } + } + } + } + } +}); +})(); + +/** Spreadsheet **/ +(function() { + +function getRowLabel(value){ + if (this.options.spreadsheet.tickFormatter){ + //TODO maybe pass the xaxis formatter to the custom tick formatter as an opt-out? + return this.options.spreadsheet.tickFormatter(value); + } + else { + var t = _.find(this.axes.x.ticks, function(t){return t.v == value;}); + if (t) { + return t.label; + } + return value; + } +} + +var + D = Flotr.DOM, + _ = Flotr._; + +Flotr.addPlugin('spreadsheet', { + options: { + show: false, // => show the data grid using two tabs + tabGraphLabel: 'Graph', + tabDataLabel: 'Data', + toolbarDownload: 'Download CSV', // @todo: add better language support + toolbarSelectAll: 'Select all', + csvFileSeparator: ',', + decimalSeparator: '.', + tickFormatter: null, + initialTab: 'graph' + }, + /** + * Builds the tabs in the DOM + */ + callbacks: { + 'flotr:afterconstruct': function(){ + // @TODO necessary? + //this.el.select('.flotr-tabs-group,.flotr-datagrid-container').invoke('remove'); + + if (!this.options.spreadsheet.show) return; + + var ss = this.spreadsheet, + container = D.node('
'), + graph = D.node('
'+this.options.spreadsheet.tabGraphLabel+'
'), + data = D.node('
'+this.options.spreadsheet.tabDataLabel+'
'), + offset; + + ss.tabsContainer = container; + ss.tabs = { graph : graph, data : data }; + + D.insert(container, graph); + D.insert(container, data); + D.insert(this.el, container); + + offset = D.size(data).height + 2; + this.plotOffset.bottom += offset; + + D.setStyles(container, {top: this.canvasHeight-offset+'px'}); + + this. + observe(graph, 'click', function(){ss.showTab('graph');}). + observe(data, 'click', function(){ss.showTab('data');}); + if (this.options.spreadsheet.initialTab !== 'graph'){ + ss.showTab(this.options.spreadsheet.initialTab); + } + } + }, + /** + * Builds a matrix of the data to make the correspondance between the x values and the y values : + * X value => Y values from the axes + * @return {Array} The data grid + */ + loadDataGrid: function(){ + if (this.seriesData) return this.seriesData; + + var s = this.series, + rows = {}; + + /* The data grid is a 2 dimensions array. There is a row for each X value. + * Each row contains the x value and the corresponding y value for each serie ('undefined' if there isn't one) + **/ + _.each(s, function(serie, i){ + _.each(serie.data, function (v) { + var x = v[0], + y = v[1], + r = rows[x]; + if (r) { + r[i+1] = y; + } else { + var newRow = []; + newRow[0] = x; + newRow[i+1] = y; + rows[x] = newRow; + } + }); + }); + + // The data grid is sorted by x value + this.seriesData = _.sortBy(rows, function(row, x){ + return parseInt(x, 10); + }); + return this.seriesData; + }, + /** + * Constructs the data table for the spreadsheet + * @todo make a spreadsheet manager (Flotr.Spreadsheet) + * @return {Element} The resulting table element + */ + constructDataGrid: function(){ + // If the data grid has already been built, nothing to do here + if (this.spreadsheet.datagrid) return this.spreadsheet.datagrid; + + var s = this.series, + datagrid = this.spreadsheet.loadDataGrid(), + colgroup = [''], + buttonDownload, buttonSelect, t; + + // First row : series' labels + var html = ['']; + html.push(''); + _.each(s, function(serie,i){ + html.push(''); + colgroup.push(''); + }); + html.push(''); + // Data rows + _.each(datagrid, function(row){ + html.push(''); + _.times(s.length+1, function(i){ + var tag = 'td', + value = row[i], + // TODO: do we really want to handle problems with floating point + // precision here? + content = (!_.isUndefined(value) ? Math.round(value*100000)/100000 : ''); + if (i === 0) { + tag = 'th'; + var label = getRowLabel.call(this, content); + if (label) content = label; + } + + html.push('<'+tag+(tag=='th'?' scope="row"':'')+'>'+content+''); + }, this); + html.push(''); + }, this); + colgroup.push(''); + t = D.node(html.join('')); + + /** + * @TODO disabled this + if (!Flotr.isIE || Flotr.isIE == 9) { + function handleMouseout(){ + t.select('colgroup col.hover, th.hover').invoke('removeClassName', 'hover'); + } + function handleMouseover(e){ + var td = e.element(), + siblings = td.previousSiblings(); + t.select('th[scope=col]')[siblings.length-1].addClassName('hover'); + t.select('colgroup col')[siblings.length].addClassName('hover'); + } + _.each(t.select('td'), function(td) { + Flotr.EventAdapter. + observe(td, 'mouseover', handleMouseover). + observe(td, 'mouseout', handleMouseout); + }); + } + */ + + buttonDownload = D.node( + ''); + + buttonSelect = D.node( + ''); + + this. + observe(buttonDownload, 'click', _.bind(this.spreadsheet.downloadCSV, this)). + observe(buttonSelect, 'click', _.bind(this.spreadsheet.selectAllData, this)); + + var toolbar = D.node('
'); + D.insert(toolbar, buttonDownload); + D.insert(toolbar, buttonSelect); + + var containerHeight =this.canvasHeight - D.size(this.spreadsheet.tabsContainer).height-2, + container = D.node('
'); + + D.insert(container, toolbar); + D.insert(container, t); + D.insert(this.el, container); + this.spreadsheet.datagrid = t; + this.spreadsheet.container = container; + + return t; + }, + /** + * Shows the specified tab, by its name + * @todo make a tab manager (Flotr.Tabs) + * @param {String} tabName - The tab name + */ + showTab: function(tabName){ + if (this.spreadsheet.activeTab === tabName){ + return; + } + switch(tabName) { + case 'graph': + D.hide(this.spreadsheet.container); + D.removeClass(this.spreadsheet.tabs.data, 'selected'); + D.addClass(this.spreadsheet.tabs.graph, 'selected'); + break; + case 'data': + if (!this.spreadsheet.datagrid) + this.spreadsheet.constructDataGrid(); + D.show(this.spreadsheet.container); + D.addClass(this.spreadsheet.tabs.data, 'selected'); + D.removeClass(this.spreadsheet.tabs.graph, 'selected'); + break; + default: + throw 'Illegal tab name: ' + tabName; + } + this.spreadsheet.activeTab = tabName; + }, + /** + * Selects the data table in the DOM for copy/paste + */ + selectAllData: function(){ + if (this.spreadsheet.tabs) { + var selection, range, doc, win, node = this.spreadsheet.constructDataGrid(); + + this.spreadsheet.showTab('data'); + + // deferred to be able to select the table + setTimeout(function () { + if ((doc = node.ownerDocument) && (win = doc.defaultView) && + win.getSelection && doc.createRange && + (selection = window.getSelection()) && + selection.removeAllRanges) { + range = doc.createRange(); + range.selectNode(node); + selection.removeAllRanges(); + selection.addRange(range); + } + else if (document.body && document.body.createTextRange && + (range = document.body.createTextRange())) { + range.moveToElementText(node); + range.select(); + } + }, 0); + return true; + } + else return false; + }, + /** + * Converts the data into CSV in order to download a file + */ + downloadCSV: function(){ + var csv = '', + series = this.series, + options = this.options, + dg = this.spreadsheet.loadDataGrid(), + separator = encodeURIComponent(options.spreadsheet.csvFileSeparator); + + if (options.spreadsheet.decimalSeparator === options.spreadsheet.csvFileSeparator) { + throw "The decimal separator is the same as the column separator ("+options.spreadsheet.decimalSeparator+")"; + } + + // The first row + _.each(series, function(serie, i){ + csv += separator+'"'+(serie.label || String.fromCharCode(65+i)).replace(/\"/g, '\\"')+'"'; + }); + + csv += "%0D%0A"; // \r\n + + // For each row + csv += _.reduce(dg, function(memo, row){ + var rowLabel = getRowLabel.call(this, row[0]) || ''; + rowLabel = '"'+(rowLabel+'').replace(/\"/g, '\\"')+'"'; + var numbers = row.slice(1).join(separator); + if (options.spreadsheet.decimalSeparator !== '.') { + numbers = numbers.replace(/\./g, options.spreadsheet.decimalSeparator); + } + return memo + rowLabel+separator+numbers+"%0D%0A"; // \t and \r\n + }, '', this); + + if (Flotr.isIE && Flotr.isIE < 9) { + csv = csv.replace(new RegExp(separator, 'g'), decodeURIComponent(separator)).replace(/%0A/g, '\n').replace(/%0D/g, '\r'); + window.open().document.write(csv); + } + else window.open('data:text/csv,'+csv); + } +}); +})(); + +(function () { + +var D = Flotr.DOM; + +Flotr.addPlugin('titles', { + callbacks: { + 'flotr:afterdraw': function() { + this.titles.drawTitles(); + } + }, + /** + * Draws the title and the subtitle + */ + drawTitles : function () { + var html, + options = this.options, + margin = options.grid.labelMargin, + ctx = this.ctx, + a = this.axes; + + if (!options.HtmlText && this.textEnabled) { + var style = { + size: options.fontSize, + color: options.grid.color, + textAlign: 'center' + }; + + // Add subtitle + if (options.subtitle){ + Flotr.drawText( + ctx, options.subtitle, + this.plotOffset.left + this.plotWidth/2, + this.titleHeight + this.subtitleHeight - 2, + style + ); + } + + style.weight = 1.5; + style.size *= 1.5; + + // Add title + if (options.title){ + Flotr.drawText( + ctx, options.title, + this.plotOffset.left + this.plotWidth/2, + this.titleHeight - 2, + style + ); + } + + style.weight = 1.8; + style.size *= 0.8; + + // Add x axis title + if (a.x.options.title && a.x.used){ + style.textAlign = a.x.options.titleAlign || 'center'; + style.textBaseline = 'top'; + style.angle = Flotr.toRad(a.x.options.titleAngle); + style = Flotr.getBestTextAlign(style.angle, style); + Flotr.drawText( + ctx, a.x.options.title, + this.plotOffset.left + this.plotWidth/2, + this.plotOffset.top + a.x.maxLabel.height + this.plotHeight + 2 * margin, + style + ); + } + + // Add x2 axis title + if (a.x2.options.title && a.x2.used){ + style.textAlign = a.x2.options.titleAlign || 'center'; + style.textBaseline = 'bottom'; + style.angle = Flotr.toRad(a.x2.options.titleAngle); + style = Flotr.getBestTextAlign(style.angle, style); + Flotr.drawText( + ctx, a.x2.options.title, + this.plotOffset.left + this.plotWidth/2, + this.plotOffset.top - a.x2.maxLabel.height - 2 * margin, + style + ); + } + + // Add y axis title + if (a.y.options.title && a.y.used){ + style.textAlign = a.y.options.titleAlign || 'right'; + style.textBaseline = 'middle'; + style.angle = Flotr.toRad(a.y.options.titleAngle); + style = Flotr.getBestTextAlign(style.angle, style); + Flotr.drawText( + ctx, a.y.options.title, + this.plotOffset.left - a.y.maxLabel.width - 2 * margin, + this.plotOffset.top + this.plotHeight / 2, + style + ); + } + + // Add y2 axis title + if (a.y2.options.title && a.y2.used){ + style.textAlign = a.y2.options.titleAlign || 'left'; + style.textBaseline = 'middle'; + style.angle = Flotr.toRad(a.y2.options.titleAngle); + style = Flotr.getBestTextAlign(style.angle, style); + Flotr.drawText( + ctx, a.y2.options.title, + this.plotOffset.left + this.plotWidth + a.y2.maxLabel.width + 2 * margin, + this.plotOffset.top + this.plotHeight / 2, + style + ); + } + } + else { + html = []; + + // Add title + if (options.title) + html.push( + '
', options.title, '
' + ); + + // Add subtitle + if (options.subtitle) + html.push( + '
', options.subtitle, '
' + ); + + html.push(''); + + html.push('
'); + + // Add x axis title + if (a.x.options.title && a.x.used) + html.push( + '
', a.x.options.title, '
' + ); + + // Add x2 axis title + if (a.x2.options.title && a.x2.used) + html.push( + '
', a.x2.options.title, '
' + ); + + // Add y axis title + if (a.y.options.title && a.y.used) + html.push( + '
', a.y.options.title, '
' + ); + + // Add y2 axis title + if (a.y2.options.title && a.y2.used) + html.push( + '
', a.y2.options.title, '
' + ); + + html = html.join(''); + + var div = D.create('div'); + D.setStyles({ + color: options.grid.color + }); + div.className = 'flotr-titles'; + D.insert(this.el, div); + D.insert(div, html); + } + } +}); +})(); diff --git a/resources/info.png b/resources/info.png new file mode 100644 index 0000000..fe222e4 Binary files /dev/null and b/resources/info.png differ diff --git a/resources/logo-squidanalyzer.png b/resources/logo-squidanalyzer.png new file mode 100644 index 0000000..6164bf8 Binary files /dev/null and b/resources/logo-squidanalyzer.png differ diff --git a/resources/network.png b/resources/network.png new file mode 100644 index 0000000..25cf464 Binary files /dev/null and b/resources/network.png differ diff --git a/resources/sorttable.js b/resources/sorttable.js new file mode 100644 index 0000000..25bccb2 --- /dev/null +++ b/resources/sorttable.js @@ -0,0 +1,493 @@ +/* + SortTable + version 2 + 7th April 2007 + Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/ + + Instructions: + Download this file + Add to your HTML + Add class="sortable" to any table you'd like to make sortable + Click on the headers to sort + + Thanks to many, many people for contributions and suggestions. + Licenced as X11: http://www.kryogenix.org/code/browser/licence.html + This basically means: do what you want with it. +*/ + + +var stIsIE = /*@cc_on!@*/false; + +sorttable = { + init: function() { + // quit if this function has already been called + if (arguments.callee.done) return; + // flag this function so we don't do the same thing twice + arguments.callee.done = true; + // kill the timer + if (_timer) clearInterval(_timer); + + if (!document.createElement || !document.getElementsByTagName) return; + + sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/; + + forEach(document.getElementsByTagName('table'), function(table) { + if (table.className.search(/\bsortable\b/) != -1) { + sorttable.makeSortable(table); + } + }); + + }, + + makeSortable: function(table) { + if (table.getElementsByTagName('thead').length == 0) { + // table doesn't have a tHead. Since it should have, create one and + // put the first table row in it. + the = document.createElement('thead'); + the.appendChild(table.rows[0]); + table.insertBefore(the,table.firstChild); + } + // Safari doesn't support table.tHead, sigh + if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0]; + + if (table.tHead.rows.length != 1) return; // can't cope with two header rows + + // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as + // "total" rows, for example). This is B&R, since what you're supposed + // to do is put them in a tfoot. So, if there are sortbottom rows, + // for backwards compatibility, move them to tfoot (creating it if needed). + sortbottomrows = []; + for (var i=0; i5' : ' ▴'; + this.appendChild(sortrevind); + return; + } + if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) { + // if we're already sorted by this column in reverse, just + // re-reverse the table, which is quicker + sorttable.reverse(this.sorttable_tbody); + this.className = this.className.replace('sorttable_sorted_reverse', + 'sorttable_sorted'); + this.removeChild(document.getElementById('sorttable_sortrevind')); + sortfwdind = document.createElement('span'); + sortfwdind.id = "sorttable_sortfwdind"; + sortfwdind.innerHTML = stIsIE ? ' 6' : ' ▾'; + this.appendChild(sortfwdind); + return; + } + + // remove sorttable_sorted classes + theadrow = this.parentNode; + forEach(theadrow.childNodes, function(cell) { + if (cell.nodeType == 1) { // an element + cell.className = cell.className.replace('sorttable_sorted_reverse',''); + cell.className = cell.className.replace('sorttable_sorted',''); + } + }); + sortfwdind = document.getElementById('sorttable_sortfwdind'); + if (sortfwdind) { sortfwdind.parentNode.removeChild(sortfwdind); } + sortrevind = document.getElementById('sorttable_sortrevind'); + if (sortrevind) { sortrevind.parentNode.removeChild(sortrevind); } + + this.className += ' sorttable_sorted'; + sortfwdind = document.createElement('span'); + sortfwdind.id = "sorttable_sortfwdind"; + sortfwdind.innerHTML = stIsIE ? ' 6' : ' ▾'; + this.appendChild(sortfwdind); + + // build an array to sort. This is a Schwartzian transform thing, + // i.e., we "decorate" each row with the actual sort key, + // sort based on the sort keys, and then put the rows back in order + // which is a lot faster because you only do getInnerText once per row + row_array = []; + col = this.sorttable_columnindex; + rows = this.sorttable_tbody.rows; + for (var j=0; j 12) { + // definitely dd/mm + return sorttable.sort_ddmm; + } else if (second > 12) { + return sorttable.sort_mmdd; + } else { + // looks like a date, but we can't tell which, so assume + // that it's dd/mm (English imperialism!) and keep looking + sortfn = sorttable.sort_ddmm; + } + } + } + } + return sortfn; + }, + + getInnerText: function(node) { + // gets the text we want to use for sorting for a cell. + // strips leading and trailing whitespace. + // this is *not* a generic getInnerText function; it's special to sorttable. + // for example, you can override the cell text with a customkey attribute. + // it also gets .value for fields. + + hasInputs = (typeof node.getElementsByTagName == 'function') && + node.getElementsByTagName('input').length; + + if (node.getAttribute("sorttable_customkey") != null) { + return node.getAttribute("sorttable_customkey"); + } + else if (typeof node.textContent != 'undefined' && !hasInputs) { + return node.textContent.replace(/^\s+|\s+$/g, ''); + } + else if (typeof node.innerText != 'undefined' && !hasInputs) { + return node.innerText.replace(/^\s+|\s+$/g, ''); + } + else if (typeof node.text != 'undefined' && !hasInputs) { + return node.text.replace(/^\s+|\s+$/g, ''); + } + else { + switch (node.nodeType) { + case 3: + if (node.nodeName.toLowerCase() == 'input') { + return node.value.replace(/^\s+|\s+$/g, ''); + } + case 4: + return node.nodeValue.replace(/^\s+|\s+$/g, ''); + break; + case 1: + case 11: + var innerText = ''; + for (var i = 0; i < node.childNodes.length; i++) { + innerText += sorttable.getInnerText(node.childNodes[i]); + } + return innerText.replace(/^\s+|\s+$/g, ''); + break; + default: + return ''; + } + } + }, + + reverse: function(tbody) { + // reverse the rows in a tbody + newrows = []; + for (var i=0; i=0; i--) { + tbody.appendChild(newrows[i]); + } + delete newrows; + }, + + /* sort functions + each sort function takes two parameters, a and b + you are comparing a[0] and b[0] */ + sort_numeric: function(a,b) { + aa = parseFloat(a[0].replace(/[^0-9.-]/g,'')); + if (isNaN(aa)) aa = 0; + bb = parseFloat(b[0].replace(/[^0-9.-]/g,'')); + if (isNaN(bb)) bb = 0; + return aa-bb; + }, + sort_alpha: function(a,b) { + if (a[0]==b[0]) return 0; + if (a[0] 0 ) { + var q = list[i]; list[i] = list[i+1]; list[i+1] = q; + swap = true; + } + } // for + t--; + + if (!swap) break; + + for(var i = t; i > b; --i) { + if ( comp_func(list[i], list[i-1]) < 0 ) { + var q = list[i]; list[i] = list[i-1]; list[i-1] = q; + swap = true; + } + } // for + b++; + + } // while(swap) + } +} + +/* ****************************************************************** + Supporting functions: bundled here to avoid depending on a library + ****************************************************************** */ + +// Dean Edwards/Matthias Miller/John Resig + +/* for Mozilla/Opera9 */ +if (document.addEventListener) { + document.addEventListener("DOMContentLoaded", sorttable.init, false); +} + +/* for Internet Explorer */ +/*@cc_on @*/ +/*@if (@_win32) + document.write("
 '+(serie.label || String.fromCharCode(65+i))+'