7008 lines
219 KiB
JavaScript
7008 lines
219 KiB
JavaScript
function toggle(idButton, idDiv, label) {
|
||
if(document.getElementById(idDiv)) {
|
||
if(document.getElementById(idDiv).style.display == 'none') {
|
||
document.getElementById(idDiv).style.display = 'block';
|
||
document.getElementById(idButton).value = 'Hide '+label;
|
||
} else {
|
||
document.getElementById(idDiv).style.display = 'none';
|
||
document.getElementById(idButton).value = 'Show '+label;
|
||
}
|
||
}
|
||
}
|
||
|
||
/*!
|
||
* 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, '<div style="'+style+'" class="'+className+' flotr-dummy-div">' + text + '</div>');
|
||
D.insert(this.o.element, div);
|
||
|
||
return D.size(div);
|
||
},
|
||
|
||
measureText : function (text, style) {
|
||
|
||
var
|
||
context = this.o.ctx,
|
||
metrics;
|
||
|
||
if (!context.fillText || (F.isIphone && context.measure)) {
|
||
return { width : context.measure(text, style)};
|
||
}
|
||
|
||
style = _.extend({
|
||
size: F.defaultOptions.fontSize,
|
||
weight: 1,
|
||
angle: 0
|
||
}, style);
|
||
|
||
context.save();
|
||
context.font = (style.weight > 1 ? "bold " : "") + (style.size*1.3) + "px sans-serif";
|
||
metrics = context.measureText(text);
|
||
context.restore();
|
||
|
||
return metrics;
|
||
}
|
||
};
|
||
|
||
Flotr.Text = Text;
|
||
|
||
})();
|
||
|
||
/**
|
||
* Flotr Graph class that plots a graph on creation.
|
||
*/
|
||
(function () {
|
||
|
||
var
|
||
D = Flotr.DOM,
|
||
E = Flotr.EventAdapter,
|
||
_ = Flotr._,
|
||
flotr = Flotr;
|
||
/**
|
||
* Flotr Graph constructor.
|
||
* @param {Element} el - element to insert the graph into
|
||
* @param {Object} data - an array or object of dataseries
|
||
* @param {Object} options - an object containing options
|
||
*/
|
||
Graph = function(el, data, options){
|
||
// Let's see if we can get away with out this [JS]
|
||
// try {
|
||
this._setEl(el);
|
||
this._initMembers();
|
||
this._initPlugins();
|
||
|
||
E.fire(this.el, 'flotr:beforeinit', [this]);
|
||
|
||
this.data = data;
|
||
this.series = flotr.Series.getSeries(data);
|
||
this._initOptions(options);
|
||
this._initGraphTypes();
|
||
this._initCanvas();
|
||
this._text = new flotr.Text({
|
||
element : this.el,
|
||
ctx : this.ctx,
|
||
html : this.options.HtmlText,
|
||
textEnabled : this.textEnabled
|
||
});
|
||
E.fire(this.el, 'flotr:afterconstruct', [this]);
|
||
this._initEvents();
|
||
|
||
this.findDataRanges();
|
||
this.calculateSpacing();
|
||
|
||
this.draw(_.bind(function() {
|
||
E.fire(this.el, 'flotr:afterinit', [this]);
|
||
}, this));
|
||
/*
|
||
try {
|
||
} catch (e) {
|
||
try {
|
||
console.error(e);
|
||
} catch (e2) {}
|
||
}*/
|
||
};
|
||
|
||
function observe (object, name, callback) {
|
||
E.observe.apply(this, arguments);
|
||
this._handles.push(arguments);
|
||
return this;
|
||
}
|
||
|
||
Graph.prototype = {
|
||
|
||
destroy: function () {
|
||
E.fire(this.el, 'flotr:destroy');
|
||
_.each(this._handles, function (handle) {
|
||
E.stopObserving.apply(this, handle);
|
||
});
|
||
this._handles = [];
|
||
this.el.graph = null;
|
||
},
|
||
|
||
observe : observe,
|
||
|
||
/**
|
||
* @deprecated
|
||
*/
|
||
_observe : observe,
|
||
|
||
processColor: function(color, options){
|
||
var o = { x1: 0, y1: 0, x2: this.plotWidth, y2: this.plotHeight, opacity: 1, ctx: this.ctx };
|
||
_.extend(o, options);
|
||
return flotr.Color.processColor(color, o);
|
||
},
|
||
/**
|
||
* Function determines the min and max values for the xaxis and yaxis.
|
||
*
|
||
* TODO logarithmic range validation (consideration of 0)
|
||
*/
|
||
findDataRanges: function(){
|
||
var a = this.axes,
|
||
xaxis, yaxis, range;
|
||
|
||
_.each(this.series, function (series) {
|
||
range = series.getRange();
|
||
if (range) {
|
||
xaxis = series.xaxis;
|
||
yaxis = series.yaxis;
|
||
xaxis.datamin = Math.min(range.xmin, xaxis.datamin);
|
||
xaxis.datamax = Math.max(range.xmax, xaxis.datamax);
|
||
yaxis.datamin = Math.min(range.ymin, yaxis.datamin);
|
||
yaxis.datamax = Math.max(range.ymax, yaxis.datamax);
|
||
xaxis.used = (xaxis.used || range.xused);
|
||
yaxis.used = (yaxis.used || range.yused);
|
||
}
|
||
}, this);
|
||
|
||
// Check for empty data, no data case (none used)
|
||
if (!a.x.used && !a.x2.used) a.x.used = true;
|
||
if (!a.y.used && !a.y2.used) a.y.used = true;
|
||
|
||
_.each(a, function (axis) {
|
||
axis.calculateRange();
|
||
});
|
||
|
||
var
|
||
types = _.keys(flotr.graphTypes),
|
||
drawn = false;
|
||
|
||
_.each(this.series, function (series) {
|
||
if (series.hide) return;
|
||
_.each(types, function (type) {
|
||
if (series[type] && series[type].show) {
|
||
this.extendRange(type, series);
|
||
drawn = true;
|
||
}
|
||
}, this);
|
||
if (!drawn) {
|
||
this.extendRange(this.options.defaultType, series);
|
||
}
|
||
}, this);
|
||
},
|
||
|
||
extendRange : function (type, series) {
|
||
if (this[type].extendRange) this[type].extendRange(series, series.data, series[type], this[type]);
|
||
if (this[type].extendYRange) this[type].extendYRange(series.yaxis, series.data, series[type], this[type]);
|
||
if (this[type].extendXRange) this[type].extendXRange(series.xaxis, series.data, series[type], this[type]);
|
||
},
|
||
|
||
/**
|
||
* Calculates axis label sizes.
|
||
*/
|
||
calculateSpacing: function(){
|
||
|
||
var a = this.axes,
|
||
options = this.options,
|
||
series = this.series,
|
||
margin = options.grid.labelMargin,
|
||
T = this._text,
|
||
x = a.x,
|
||
x2 = a.x2,
|
||
y = a.y,
|
||
y2 = a.y2,
|
||
maxOutset = options.grid.outlineWidth,
|
||
i, j, l, dim;
|
||
|
||
// TODO post refactor, fix this
|
||
_.each(a, function (axis) {
|
||
axis.calculateTicks();
|
||
axis.calculateTextDimensions(T, options);
|
||
});
|
||
|
||
// Title height
|
||
dim = T.dimensions(
|
||
options.title,
|
||
{size: options.fontSize*1.5},
|
||
'font-size:1em;font-weight:bold;',
|
||
'flotr-title'
|
||
);
|
||
this.titleHeight = dim.height;
|
||
|
||
// Subtitle height
|
||
dim = T.dimensions(
|
||
options.subtitle,
|
||
{size: options.fontSize},
|
||
'font-size:smaller;',
|
||
'flotr-subtitle'
|
||
);
|
||
this.subtitleHeight = dim.height;
|
||
|
||
for(j = 0; j < options.length; ++j){
|
||
if (series[j].points.show){
|
||
maxOutset = Math.max(maxOutset, series[j].points.radius + series[j].points.lineWidth/2);
|
||
}
|
||
}
|
||
|
||
var p = this.plotOffset;
|
||
if (x.options.margin === false) {
|
||
p.bottom = 0;
|
||
p.top = 0;
|
||
} else {
|
||
p.bottom += (options.grid.circular ? 0 : (x.used && x.options.showLabels ? (x.maxLabel.height + margin) : 0)) +
|
||
(x.used && x.options.title ? (x.titleSize.height + margin) : 0) + maxOutset;
|
||
|
||
p.top += (options.grid.circular ? 0 : (x2.used && x2.options.showLabels ? (x2.maxLabel.height + margin) : 0)) +
|
||
(x2.used && x2.options.title ? (x2.titleSize.height + margin) : 0) + this.subtitleHeight + this.titleHeight + maxOutset;
|
||
}
|
||
if (y.options.margin === false) {
|
||
p.left = 0;
|
||
p.right = 0;
|
||
} else {
|
||
p.left += (options.grid.circular ? 0 : (y.used && y.options.showLabels ? (y.maxLabel.width + margin) : 0)) +
|
||
(y.used && y.options.title ? (y.titleSize.width + margin) : 0) + maxOutset;
|
||
|
||
p.right += (options.grid.circular ? 0 : (y2.used && y2.options.showLabels ? (y2.maxLabel.width + margin) : 0)) +
|
||
(y2.used && y2.options.title ? (y2.titleSize.width + margin) : 0) + maxOutset;
|
||
}
|
||
|
||
p.top = Math.floor(p.top); // In order the outline not to be blured
|
||
|
||
this.plotWidth = this.canvasWidth - p.left - p.right;
|
||
this.plotHeight = this.canvasHeight - p.bottom - p.top;
|
||
|
||
// TODO post refactor, fix this
|
||
x.length = x2.length = this.plotWidth;
|
||
y.length = y2.length = this.plotHeight;
|
||
y.offset = y2.offset = this.plotHeight;
|
||
x.setScale();
|
||
x2.setScale();
|
||
y.setScale();
|
||
y2.setScale();
|
||
},
|
||
/**
|
||
* Draws grid, labels, series and outline.
|
||
*/
|
||
draw: function(after) {
|
||
|
||
var
|
||
context = this.ctx,
|
||
i;
|
||
|
||
E.fire(this.el, 'flotr:beforedraw', [this.series, this]);
|
||
|
||
if (this.series.length) {
|
||
|
||
context.save();
|
||
context.translate(this.plotOffset.left, this.plotOffset.top);
|
||
|
||
for (i = 0; i < this.series.length; i++) {
|
||
if (!this.series[i].hide) this.drawSeries(this.series[i]);
|
||
}
|
||
|
||
context.restore();
|
||
this.clip();
|
||
}
|
||
|
||
E.fire(this.el, 'flotr:afterdraw', [this.series, this]);
|
||
if (after) after();
|
||
},
|
||
/**
|
||
* Actually draws the graph.
|
||
* @param {Object} series - series to draw
|
||
*/
|
||
drawSeries: function(series){
|
||
|
||
function drawChart (series, typeKey) {
|
||
var options = this.getOptions(series, typeKey);
|
||
this[typeKey].draw(options);
|
||
}
|
||
|
||
var drawn = false;
|
||
series = series || this.series;
|
||
|
||
_.each(flotr.graphTypes, function (type, typeKey) {
|
||
if (series[typeKey] && series[typeKey].show && this[typeKey]) {
|
||
drawn = true;
|
||
drawChart.call(this, series, typeKey);
|
||
}
|
||
}, this);
|
||
|
||
if (!drawn) drawChart.call(this, series, this.options.defaultType);
|
||
},
|
||
|
||
getOptions : function (series, typeKey) {
|
||
var
|
||
type = series[typeKey],
|
||
graphType = this[typeKey],
|
||
xaxis = series.xaxis,
|
||
yaxis = series.yaxis,
|
||
options = {
|
||
context : this.ctx,
|
||
width : this.plotWidth,
|
||
height : this.plotHeight,
|
||
fontSize : this.options.fontSize,
|
||
fontColor : this.options.fontColor,
|
||
textEnabled : this.textEnabled,
|
||
htmlText : this.options.HtmlText,
|
||
text : this._text, // TODO Is this necessary?
|
||
element : this.el,
|
||
data : series.data,
|
||
color : series.color,
|
||
shadowSize : series.shadowSize,
|
||
xScale : xaxis.d2p,
|
||
yScale : yaxis.d2p,
|
||
xInverse : xaxis.p2d,
|
||
yInverse : yaxis.p2d
|
||
};
|
||
|
||
options = flotr.merge(type, options);
|
||
|
||
// Fill
|
||
options.fillStyle = this.processColor(
|
||
type.fillColor || series.color,
|
||
{opacity: type.fillOpacity}
|
||
);
|
||
|
||
return options;
|
||
},
|
||
/**
|
||
* Calculates the coordinates from a mouse event object.
|
||
* @param {Event} event - Mouse Event object.
|
||
* @return {Object} Object with coordinates of the mouse.
|
||
*/
|
||
getEventPosition: function (e){
|
||
|
||
var
|
||
d = document,
|
||
b = d.body,
|
||
de = d.documentElement,
|
||
axes = this.axes,
|
||
plotOffset = this.plotOffset,
|
||
lastMousePos = this.lastMousePos,
|
||
pointer = E.eventPointer(e),
|
||
dx = pointer.x - lastMousePos.pageX,
|
||
dy = pointer.y - lastMousePos.pageY,
|
||
r, rx, ry;
|
||
|
||
if ('ontouchstart' in this.el) {
|
||
r = D.position(this.overlay);
|
||
rx = pointer.x - r.left - plotOffset.left;
|
||
ry = pointer.y - r.top - plotOffset.top;
|
||
} else {
|
||
r = this.overlay.getBoundingClientRect();
|
||
rx = e.clientX - r.left - plotOffset.left - b.scrollLeft - de.scrollLeft;
|
||
ry = e.clientY - r.top - plotOffset.top - b.scrollTop - de.scrollTop;
|
||
}
|
||
|
||
return {
|
||
x: axes.x.p2d(rx),
|
||
x2: axes.x2.p2d(rx),
|
||
y: axes.y.p2d(ry),
|
||
y2: axes.y2.p2d(ry),
|
||
relX: rx,
|
||
relY: ry,
|
||
dX: dx,
|
||
dY: dy,
|
||
absX: pointer.x,
|
||
absY: pointer.y,
|
||
pageX: pointer.x,
|
||
pageY: pointer.y
|
||
};
|
||
},
|
||
/**
|
||
* Observes the 'click' event and fires the 'flotr:click' event.
|
||
* @param {Event} event - 'click' Event object.
|
||
*/
|
||
clickHandler: function(event){
|
||
if(this.ignoreClick){
|
||
this.ignoreClick = false;
|
||
return this.ignoreClick;
|
||
}
|
||
E.fire(this.el, 'flotr:click', [this.getEventPosition(event), this]);
|
||
},
|
||
/**
|
||
* Observes mouse movement over the graph area. Fires the 'flotr:mousemove' event.
|
||
* @param {Event} event - 'mousemove' Event object.
|
||
*/
|
||
mouseMoveHandler: function(event){
|
||
if (this.mouseDownMoveHandler) return;
|
||
var pos = this.getEventPosition(event);
|
||
E.fire(this.el, 'flotr:mousemove', [event, pos, this]);
|
||
this.lastMousePos = pos;
|
||
},
|
||
/**
|
||
* Observes the 'mousedown' event.
|
||
* @param {Event} event - 'mousedown' Event object.
|
||
*/
|
||
mouseDownHandler: function (event){
|
||
|
||
/*
|
||
// @TODO Context menu?
|
||
if(event.isRightClick()) {
|
||
event.stop();
|
||
|
||
var overlay = this.overlay;
|
||
overlay.hide();
|
||
|
||
function cancelContextMenu () {
|
||
overlay.show();
|
||
E.stopObserving(document, 'mousemove', cancelContextMenu);
|
||
}
|
||
E.observe(document, 'mousemove', cancelContextMenu);
|
||
return;
|
||
}
|
||
*/
|
||
|
||
if (this.mouseUpHandler) return;
|
||
this.mouseUpHandler = _.bind(function (e) {
|
||
E.stopObserving(document, 'mouseup', this.mouseUpHandler);
|
||
E.stopObserving(document, 'mousemove', this.mouseDownMoveHandler);
|
||
this.mouseDownMoveHandler = null;
|
||
this.mouseUpHandler = null;
|
||
// @TODO why?
|
||
//e.stop();
|
||
E.fire(this.el, 'flotr:mouseup', [e, this]);
|
||
}, this);
|
||
this.mouseDownMoveHandler = _.bind(function (e) {
|
||
var pos = this.getEventPosition(e);
|
||
E.fire(this.el, 'flotr:mousemove', [event, pos, this]);
|
||
this.lastMousePos = pos;
|
||
}, this);
|
||
E.observe(document, 'mouseup', this.mouseUpHandler);
|
||
E.observe(document, 'mousemove', this.mouseDownMoveHandler);
|
||
E.fire(this.el, 'flotr:mousedown', [event, this]);
|
||
this.ignoreClick = false;
|
||
},
|
||
drawTooltip: function(content, x, y, options) {
|
||
var mt = this.getMouseTrack(),
|
||
style = 'opacity:0.7;background-color:#000;color:#fff;display:none;position:absolute;padding:2px 8px;-moz-border-radius:4px;border-radius:4px;white-space:nowrap;',
|
||
p = options.position,
|
||
m = options.margin,
|
||
plotOffset = this.plotOffset;
|
||
|
||
if(x !== null && y !== null){
|
||
if (!options.relative) { // absolute to the canvas
|
||
if(p.charAt(0) == 'n') style += 'top:' + (m + plotOffset.top) + 'px;bottom:auto;';
|
||
else if(p.charAt(0) == 's') style += 'bottom:' + (m + plotOffset.bottom) + 'px;top:auto;';
|
||
if(p.charAt(1) == 'e') style += 'right:' + (m + plotOffset.right) + 'px;left:auto;';
|
||
else if(p.charAt(1) == 'w') style += 'left:' + (m + plotOffset.left) + 'px;right:auto;';
|
||
}
|
||
else { // relative to the mouse
|
||
if(p.charAt(0) == 'n') style += 'bottom:' + (m - plotOffset.top - y + this.canvasHeight) + 'px;top:auto;';
|
||
else if(p.charAt(0) == 's') style += 'top:' + (m + plotOffset.top + y) + 'px;bottom:auto;';
|
||
if(p.charAt(1) == 'e') style += 'left:' + (m + plotOffset.left + x) + 'px;right:auto;';
|
||
else if(p.charAt(1) == 'w') style += 'right:' + (m - plotOffset.left - x + this.canvasWidth) + 'px;left:auto;';
|
||
}
|
||
|
||
mt.style.cssText = style;
|
||
D.empty(mt);
|
||
D.insert(mt, content);
|
||
D.show(mt);
|
||
}
|
||
else {
|
||
D.hide(mt);
|
||
}
|
||
},
|
||
|
||
clip: function (ctx) {
|
||
|
||
var
|
||
o = this.plotOffset,
|
||
w = this.canvasWidth,
|
||
h = this.canvasHeight;
|
||
|
||
ctx = ctx || this.ctx;
|
||
|
||
if (flotr.isIE && flotr.isIE < 9) {
|
||
// Clipping for excanvas :-(
|
||
ctx.save();
|
||
ctx.fillStyle = this.processColor(this.options.ieBackgroundColor);
|
||
ctx.fillRect(0, 0, w, o.top);
|
||
ctx.fillRect(0, 0, o.left, h);
|
||
ctx.fillRect(0, h - o.bottom, w, o.bottom);
|
||
ctx.fillRect(w - o.right, 0, o.right,h);
|
||
ctx.restore();
|
||
} else {
|
||
ctx.clearRect(0, 0, w, o.top);
|
||
ctx.clearRect(0, 0, o.left, h);
|
||
ctx.clearRect(0, h - o.bottom, w, o.bottom);
|
||
ctx.clearRect(w - o.right, 0, o.right,h);
|
||
}
|
||
},
|
||
|
||
_initMembers: function() {
|
||
this._handles = [];
|
||
this.lastMousePos = {pageX: null, pageY: null };
|
||
this.plotOffset = {left: 0, right: 0, top: 0, bottom: 0};
|
||
this.ignoreClick = true;
|
||
this.prevHit = null;
|
||
},
|
||
|
||
_initGraphTypes: function() {
|
||
_.each(flotr.graphTypes, function(handler, graphType){
|
||
this[graphType] = flotr.clone(handler);
|
||
}, this);
|
||
},
|
||
|
||
_initEvents: function () {
|
||
|
||
var
|
||
el = this.el,
|
||
touchendHandler, movement, touchend;
|
||
|
||
if ('ontouchstart' in el) {
|
||
|
||
touchendHandler = _.bind(function (e) {
|
||
touchend = true;
|
||
E.stopObserving(document, 'touchend', touchendHandler);
|
||
E.fire(el, 'flotr:mouseup', [event, this]);
|
||
this.multitouches = null;
|
||
|
||
if (!movement) {
|
||
this.clickHandler(e);
|
||
}
|
||
}, this);
|
||
|
||
this.observe(this.overlay, 'touchstart', _.bind(function (e) {
|
||
movement = false;
|
||
touchend = false;
|
||
this.ignoreClick = false;
|
||
|
||
if (e.touches && e.touches.length > 1) {
|
||
this.multitouches = e.touches;
|
||
}
|
||
|
||
E.fire(el, 'flotr:mousedown', [event, this]);
|
||
this.observe(document, 'touchend', touchendHandler);
|
||
}, this));
|
||
|
||
this.observe(this.overlay, 'touchmove', _.bind(function (e) {
|
||
|
||
var pos = this.getEventPosition(e);
|
||
|
||
if (this.options.preventDefault) {
|
||
e.preventDefault();
|
||
}
|
||
|
||
movement = true;
|
||
|
||
if (this.multitouches || (e.touches && e.touches.length > 1)) {
|
||
this.multitouches = e.touches;
|
||
} else {
|
||
if (!touchend) {
|
||
E.fire(el, 'flotr:mousemove', [event, pos, this]);
|
||
}
|
||
}
|
||
this.lastMousePos = pos;
|
||
}, this));
|
||
|
||
} else {
|
||
this.
|
||
observe(this.overlay, 'mousedown', _.bind(this.mouseDownHandler, this)).
|
||
observe(el, 'mousemove', _.bind(this.mouseMoveHandler, this)).
|
||
observe(this.overlay, 'click', _.bind(this.clickHandler, this)).
|
||
observe(el, 'mouseout', function () {
|
||
E.fire(el, 'flotr:mouseout');
|
||
});
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Initializes the canvas and it's overlay canvas element. When the browser is IE, this makes use
|
||
* of excanvas. The overlay canvas is inserted for displaying interactions. After the canvas elements
|
||
* are created, the elements are inserted into the container element.
|
||
*/
|
||
_initCanvas: function(){
|
||
var el = this.el,
|
||
o = this.options,
|
||
children = el.children,
|
||
removedChildren = [],
|
||
child, i,
|
||
size, style;
|
||
|
||
// Empty the el
|
||
for (i = children.length; i--;) {
|
||
child = children[i];
|
||
if (!this.canvas && child.className === 'flotr-canvas') {
|
||
this.canvas = child;
|
||
} else if (!this.overlay && child.className === 'flotr-overlay') {
|
||
this.overlay = child;
|
||
} else {
|
||
removedChildren.push(child);
|
||
}
|
||
}
|
||
for (i = removedChildren.length; i--;) {
|
||
el.removeChild(removedChildren[i]);
|
||
}
|
||
|
||
D.setStyles(el, {position: 'relative'}); // For positioning labels and overlay.
|
||
size = {};
|
||
size.width = el.clientWidth;
|
||
size.height = el.clientHeight;
|
||
|
||
if(size.width <= 0 || size.height <= 0 || o.resolution <= 0){
|
||
throw 'Invalid dimensions for plot, width = ' + size.width + ', height = ' + size.height + ', resolution = ' + o.resolution;
|
||
}
|
||
|
||
// Main canvas for drawing graph types
|
||
this.canvas = getCanvas(this.canvas, 'canvas');
|
||
// Overlay canvas for interactive features
|
||
this.overlay = getCanvas(this.overlay, 'overlay');
|
||
this.ctx = getContext(this.canvas);
|
||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||
this.octx = getContext(this.overlay);
|
||
this.octx.clearRect(0, 0, this.overlay.width, this.overlay.height);
|
||
this.canvasHeight = size.height;
|
||
this.canvasWidth = size.width;
|
||
this.textEnabled = !!this.ctx.drawText || !!this.ctx.fillText; // Enable text functions
|
||
|
||
function getCanvas(canvas, name){
|
||
if(!canvas){
|
||
canvas = D.create('canvas');
|
||
if (typeof FlashCanvas != "undefined" && typeof canvas.getContext === 'function') {
|
||
FlashCanvas.initElement(canvas);
|
||
}
|
||
canvas.className = 'flotr-'+name;
|
||
canvas.style.cssText = 'position:absolute;left:0px;top:0px;';
|
||
D.insert(el, canvas);
|
||
}
|
||
_.each(size, function(size, attribute){
|
||
D.show(canvas);
|
||
if (name == 'canvas' && canvas.getAttribute(attribute) === size) {
|
||
return;
|
||
}
|
||
canvas.setAttribute(attribute, size * o.resolution);
|
||
canvas.style[attribute] = size + 'px';
|
||
});
|
||
canvas.context_ = null; // Reset the ExCanvas context
|
||
return canvas;
|
||
}
|
||
|
||
function getContext(canvas){
|
||
if(window.G_vmlCanvasManager) window.G_vmlCanvasManager.initElement(canvas); // For ExCanvas
|
||
var context = canvas.getContext('2d');
|
||
if(!window.G_vmlCanvasManager) context.scale(o.resolution, o.resolution);
|
||
return context;
|
||
}
|
||
},
|
||
|
||
_initPlugins: function(){
|
||
// TODO Should be moved to flotr and mixed in.
|
||
_.each(flotr.plugins, function(plugin, name){
|
||
_.each(plugin.callbacks, function(fn, c){
|
||
this.observe(this.el, c, _.bind(fn, this));
|
||
}, this);
|
||
this[name] = flotr.clone(plugin);
|
||
_.each(this[name], function(fn, p){
|
||
if (_.isFunction(fn))
|
||
this[name][p] = _.bind(fn, this);
|
||
}, this);
|
||
}, this);
|
||
},
|
||
|
||
/**
|
||
* Sets options and initializes some variables and color specific values, used by the constructor.
|
||
* @param {Object} opts - options object
|
||
*/
|
||
_initOptions: function(opts){
|
||
var options = flotr.clone(flotr.defaultOptions);
|
||
options.x2axis = _.extend(_.clone(options.xaxis), options.x2axis);
|
||
options.y2axis = _.extend(_.clone(options.yaxis), options.y2axis);
|
||
this.options = flotr.merge(opts || {}, options);
|
||
|
||
if (this.options.grid.minorVerticalLines === null &&
|
||
this.options.xaxis.scaling === 'logarithmic') {
|
||
this.options.grid.minorVerticalLines = true;
|
||
}
|
||
if (this.options.grid.minorHorizontalLines === null &&
|
||
this.options.yaxis.scaling === 'logarithmic') {
|
||
this.options.grid.minorHorizontalLines = true;
|
||
}
|
||
|
||
E.fire(this.el, 'flotr:afterinitoptions', [this]);
|
||
|
||
this.axes = flotr.Axis.getAxes(this.options);
|
||
|
||
// Initialize some variables used throughout this function.
|
||
var assignedColors = [],
|
||
colors = [],
|
||
ln = this.series.length,
|
||
neededColors = this.series.length,
|
||
oc = this.options.colors,
|
||
usedColors = [],
|
||
variation = 0,
|
||
c, i, j, s;
|
||
|
||
// Collect user-defined colors from series.
|
||
for(i = neededColors - 1; i > -1; --i){
|
||
c = this.series[i].color;
|
||
if(c){
|
||
--neededColors;
|
||
if(_.isNumber(c)) assignedColors.push(c);
|
||
else usedColors.push(flotr.Color.parse(c));
|
||
}
|
||
}
|
||
|
||
// Calculate the number of colors that need to be generated.
|
||
for(i = assignedColors.length - 1; i > -1; --i)
|
||
neededColors = Math.max(neededColors, assignedColors[i] + 1);
|
||
|
||
// Generate needed number of colors.
|
||
for(i = 0; colors.length < neededColors;){
|
||
c = (oc.length == i) ? new flotr.Color(100, 100, 100) : flotr.Color.parse(oc[i]);
|
||
|
||
// Make sure each serie gets a different color.
|
||
var sign = variation % 2 == 1 ? -1 : 1,
|
||
factor = 1 + sign * Math.ceil(variation / 2) * 0.2;
|
||
c.scale(factor, factor, factor);
|
||
|
||
/**
|
||
* @todo if we're getting too close to something else, we should probably skip this one
|
||
*/
|
||
colors.push(c);
|
||
|
||
if(++i >= oc.length){
|
||
i = 0;
|
||
++variation;
|
||
}
|
||
}
|
||
|
||
// Fill the options with the generated colors.
|
||
for(i = 0, j = 0; i < ln; ++i){
|
||
s = this.series[i];
|
||
|
||
// Assign the color.
|
||
if (!s.color){
|
||
s.color = colors[j++].toString();
|
||
}else if(_.isNumber(s.color)){
|
||
s.color = colors[s.color].toString();
|
||
}
|
||
|
||
// Every series needs an axis
|
||
if (!s.xaxis) s.xaxis = this.axes.x;
|
||
if (s.xaxis == 1) s.xaxis = this.axes.x;
|
||
else if (s.xaxis == 2) s.xaxis = this.axes.x2;
|
||
|
||
if (!s.yaxis) s.yaxis = this.axes.y;
|
||
if (s.yaxis == 1) s.yaxis = this.axes.y;
|
||
else if (s.yaxis == 2) s.yaxis = this.axes.y2;
|
||
|
||
// Apply missing options to the series.
|
||
for (var t in flotr.graphTypes){
|
||
s[t] = _.extend(_.clone(this.options[t]), s[t]);
|
||
}
|
||
s.mouse = _.extend(_.clone(this.options.mouse), s.mouse);
|
||
|
||
if (_.isUndefined(s.shadowSize)) s.shadowSize = this.options.shadowSize;
|
||
}
|
||
},
|
||
|
||
_setEl: function(el) {
|
||
if (!el) throw 'The target container doesn\'t exist';
|
||
else if (el.graph instanceof Graph) el.graph.destroy();
|
||
else if (!el.clientWidth) throw 'The target container must be visible';
|
||
|
||
el.graph = this;
|
||
this.el = el;
|
||
}
|
||
};
|
||
|
||
Flotr.Graph = Graph;
|
||
|
||
})();
|
||
|
||
/**
|
||
* Flotr Axis Library
|
||
*/
|
||
|
||
(function () {
|
||
|
||
var
|
||
_ = Flotr._,
|
||
LOGARITHMIC = 'logarithmic';
|
||
|
||
function Axis (o) {
|
||
|
||
this.orientation = 1;
|
||
this.offset = 0;
|
||
this.datamin = Number.MAX_VALUE;
|
||
this.datamax = -Number.MAX_VALUE;
|
||
|
||
_.extend(this, o);
|
||
}
|
||
|
||
|
||
// Prototype
|
||
Axis.prototype = {
|
||
|
||
setScale : function () {
|
||
var
|
||
length = this.length,
|
||
max = this.max,
|
||
min = this.min,
|
||
offset = this.offset,
|
||
orientation = this.orientation,
|
||
options = this.options,
|
||
logarithmic = options.scaling === LOGARITHMIC,
|
||
scale;
|
||
|
||
if (logarithmic) {
|
||
scale = length / (log(max, options.base) - log(min, options.base));
|
||
} else {
|
||
scale = length / (max - min);
|
||
}
|
||
this.scale = scale;
|
||
|
||
// Logarithmic?
|
||
if (logarithmic) {
|
||
this.d2p = function (dataValue) {
|
||
return offset + orientation * (log(dataValue, options.base) - log(min, options.base)) * scale;
|
||
};
|
||
this.p2d = function (pointValue) {
|
||
return exp((offset + orientation * pointValue) / scale + log(min, options.base), options.base);
|
||
};
|
||
} else {
|
||
this.d2p = function (dataValue) {
|
||
return offset + orientation * (dataValue - min) * scale;
|
||
};
|
||
this.p2d = function (pointValue) {
|
||
return (offset + orientation * pointValue) / scale + min;
|
||
};
|
||
}
|
||
},
|
||
|
||
calculateTicks : function () {
|
||
var options = this.options;
|
||
|
||
this.ticks = [];
|
||
this.minorTicks = [];
|
||
|
||
// User Ticks
|
||
if(options.ticks){
|
||
this._cleanUserTicks(options.ticks, this.ticks);
|
||
this._cleanUserTicks(options.minorTicks || [], this.minorTicks);
|
||
}
|
||
else {
|
||
if (options.mode == 'time') {
|
||
this._calculateTimeTicks();
|
||
} else if (options.scaling === 'logarithmic') {
|
||
this._calculateLogTicks();
|
||
} else {
|
||
this._calculateTicks();
|
||
}
|
||
}
|
||
|
||
// Ticks to strings
|
||
_.each(this.ticks, function (tick) { tick.label += ''; });
|
||
_.each(this.minorTicks, function (tick) { tick.label += ''; });
|
||
},
|
||
|
||
/**
|
||
* Calculates the range of an axis to apply autoscaling.
|
||
*/
|
||
calculateRange: function () {
|
||
|
||
if (!this.used) return;
|
||
|
||
var axis = this,
|
||
o = axis.options,
|
||
min = o.min !== null ? o.min : axis.datamin,
|
||
max = o.max !== null ? o.max : axis.datamax,
|
||
margin = o.autoscaleMargin;
|
||
|
||
if (o.scaling == 'logarithmic') {
|
||
if (min <= 0) min = axis.datamin;
|
||
|
||
// Let it widen later on
|
||
if (max <= 0) max = min;
|
||
}
|
||
|
||
if (max == min) {
|
||
var widen = max ? 0.01 : 1.00;
|
||
if (o.min === null) min -= widen;
|
||
if (o.max === null) max += widen;
|
||
}
|
||
|
||
if (o.scaling === 'logarithmic') {
|
||
if (min < 0) min = max / o.base; // Could be the result of widening
|
||
|
||
var maxexp = Math.log(max);
|
||
if (o.base != Math.E) maxexp /= Math.log(o.base);
|
||
maxexp = Math.ceil(maxexp);
|
||
|
||
var minexp = Math.log(min);
|
||
if (o.base != Math.E) minexp /= Math.log(o.base);
|
||
minexp = Math.ceil(minexp);
|
||
|
||
axis.tickSize = Flotr.getTickSize(o.noTicks, minexp, maxexp, o.tickDecimals === null ? 0 : o.tickDecimals);
|
||
|
||
// Try to determine a suitable amount of miniticks based on the length of a decade
|
||
if (o.minorTickFreq === null) {
|
||
if (maxexp - minexp > 10)
|
||
o.minorTickFreq = 0;
|
||
else if (maxexp - minexp > 5)
|
||
o.minorTickFreq = 2;
|
||
else
|
||
o.minorTickFreq = 5;
|
||
}
|
||
} else {
|
||
axis.tickSize = Flotr.getTickSize(o.noTicks, min, max, o.tickDecimals);
|
||
}
|
||
|
||
axis.min = min;
|
||
axis.max = max; //extendRange may use axis.min or axis.max, so it should be set before it is caled
|
||
|
||
// Autoscaling. @todo This probably fails with log scale. Find a testcase and fix it
|
||
if(o.min === null && o.autoscale){
|
||
axis.min -= axis.tickSize * margin;
|
||
// Make sure we don't go below zero if all values are positive.
|
||
if(axis.min < 0 && axis.datamin >= 0) axis.min = 0;
|
||
axis.min = axis.tickSize * Math.floor(axis.min / axis.tickSize);
|
||
}
|
||
|
||
if(o.max === null && o.autoscale){
|
||
axis.max += axis.tickSize * margin;
|
||
if(axis.max > 0 && axis.datamax <= 0 && axis.datamax != axis.datamin) axis.max = 0;
|
||
axis.max = axis.tickSize * Math.ceil(axis.max / axis.tickSize);
|
||
}
|
||
|
||
if (axis.min == axis.max) axis.max = axis.min + 1;
|
||
},
|
||
|
||
calculateTextDimensions : function (T, options) {
|
||
|
||
var maxLabel = '',
|
||
length,
|
||
i;
|
||
|
||
if (this.options.showLabels) {
|
||
for (i = 0; i < this.ticks.length; ++i) {
|
||
length = this.ticks[i].label.length;
|
||
if (length > maxLabel.length){
|
||
maxLabel = this.ticks[i].label;
|
||
}
|
||
}
|
||
}
|
||
|
||
this.maxLabel = T.dimensions(
|
||
maxLabel,
|
||
{size:options.fontSize, angle: Flotr.toRad(this.options.labelsAngle)},
|
||
'font-size:smaller;',
|
||
'flotr-grid-label'
|
||
);
|
||
|
||
this.titleSize = T.dimensions(
|
||
this.options.title,
|
||
{size:options.fontSize*1.2, angle: Flotr.toRad(this.options.titleAngle)},
|
||
'font-weight:bold;',
|
||
'flotr-axis-title'
|
||
);
|
||
},
|
||
|
||
_cleanUserTicks : function (ticks, axisTicks) {
|
||
|
||
var axis = this, options = this.options,
|
||
v, i, label, tick;
|
||
|
||
if(_.isFunction(ticks)) ticks = ticks({min : axis.min, max : axis.max});
|
||
|
||
for(i = 0; i < ticks.length; ++i){
|
||
tick = ticks[i];
|
||
if(typeof(tick) === 'object'){
|
||
v = tick[0];
|
||
label = (tick.length > 1) ? tick[1] : options.tickFormatter(v, {min : axis.min, max : axis.max});
|
||
} else {
|
||
v = tick;
|
||
label = options.tickFormatter(v, {min : this.min, max : this.max});
|
||
}
|
||
axisTicks[i] = { v: v, label: label };
|
||
}
|
||
},
|
||
|
||
_calculateTimeTicks : function () {
|
||
this.ticks = Flotr.Date.generator(this);
|
||
},
|
||
|
||
_calculateLogTicks : function () {
|
||
|
||
var axis = this,
|
||
o = axis.options,
|
||
v,
|
||
decadeStart;
|
||
|
||
var max = Math.log(axis.max);
|
||
if (o.base != Math.E) max /= Math.log(o.base);
|
||
max = Math.ceil(max);
|
||
|
||
var min = Math.log(axis.min);
|
||
if (o.base != Math.E) min /= Math.log(o.base);
|
||
min = Math.ceil(min);
|
||
|
||
for (i = min; i < max; i += axis.tickSize) {
|
||
decadeStart = (o.base == Math.E) ? Math.exp(i) : Math.pow(o.base, i);
|
||
// Next decade begins here:
|
||
var decadeEnd = decadeStart * ((o.base == Math.E) ? Math.exp(axis.tickSize) : Math.pow(o.base, axis.tickSize));
|
||
var stepSize = (decadeEnd - decadeStart) / o.minorTickFreq;
|
||
|
||
axis.ticks.push({v: decadeStart, label: o.tickFormatter(decadeStart, {min : axis.min, max : axis.max})});
|
||
for (v = decadeStart + stepSize; v < decadeEnd; v += stepSize)
|
||
axis.minorTicks.push({v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max})});
|
||
}
|
||
|
||
// Always show the value at the would-be start of next decade (end of this decade)
|
||
decadeStart = (o.base == Math.E) ? Math.exp(i) : Math.pow(o.base, i);
|
||
axis.ticks.push({v: decadeStart, label: o.tickFormatter(decadeStart, {min : axis.min, max : axis.max})});
|
||
},
|
||
|
||
_calculateTicks : function () {
|
||
|
||
var axis = this,
|
||
o = axis.options,
|
||
tickSize = axis.tickSize,
|
||
min = axis.min,
|
||
max = axis.max,
|
||
start = tickSize * Math.ceil(min / tickSize), // Round to nearest multiple of tick size.
|
||
decimals,
|
||
minorTickSize,
|
||
v, v2,
|
||
i, j;
|
||
|
||
if (o.minorTickFreq)
|
||
minorTickSize = tickSize / o.minorTickFreq;
|
||
|
||
// Then store all possible ticks.
|
||
for (i = 0; (v = v2 = start + i * tickSize) <= max; ++i){
|
||
|
||
// Round (this is always needed to fix numerical instability).
|
||
decimals = o.tickDecimals;
|
||
if (decimals === null) decimals = 1 - Math.floor(Math.log(tickSize) / Math.LN10);
|
||
if (decimals < 0) decimals = 0;
|
||
|
||
v = v.toFixed(decimals);
|
||
axis.ticks.push({ v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max}) });
|
||
|
||
if (o.minorTickFreq) {
|
||
for (j = 0; j < o.minorTickFreq && (i * tickSize + j * minorTickSize) < max; ++j) {
|
||
v = v2 + j * minorTickSize;
|
||
axis.minorTicks.push({ v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max}) });
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
};
|
||
|
||
|
||
// Static Methods
|
||
_.extend(Axis, {
|
||
getAxes : function (options) {
|
||
return {
|
||
x: new Axis({options: options.xaxis, n: 1, length: this.plotWidth}),
|
||
x2: new Axis({options: options.x2axis, n: 2, length: this.plotWidth}),
|
||
y: new Axis({options: options.yaxis, n: 1, length: this.plotHeight, offset: this.plotHeight, orientation: -1}),
|
||
y2: new Axis({options: options.y2axis, n: 2, length: this.plotHeight, offset: this.plotHeight, orientation: -1})
|
||
};
|
||
}
|
||
});
|
||
|
||
|
||
// Helper Methods
|
||
|
||
|
||
function log (value, base) {
|
||
value = Math.log(Math.max(value, Number.MIN_VALUE));
|
||
if (base !== Math.E)
|
||
value /= Math.log(base);
|
||
return value;
|
||
}
|
||
|
||
function exp (value, base) {
|
||
return (base === Math.E) ? Math.exp(value) : Math.pow(base, value);
|
||
}
|
||
|
||
Flotr.Axis = Axis;
|
||
|
||
})();
|
||
|
||
/**
|
||
* Flotr Series Library
|
||
*/
|
||
|
||
(function () {
|
||
|
||
var
|
||
_ = Flotr._;
|
||
|
||
function Series (o) {
|
||
_.extend(this, o);
|
||
}
|
||
|
||
Series.prototype = {
|
||
|
||
getRange: function () {
|
||
|
||
var
|
||
data = this.data,
|
||
length = data.length,
|
||
xmin = Number.MAX_VALUE,
|
||
ymin = Number.MAX_VALUE,
|
||
xmax = -Number.MAX_VALUE,
|
||
ymax = -Number.MAX_VALUE,
|
||
xused = false,
|
||
yused = false,
|
||
x, y, i;
|
||
|
||
if (length < 0 || this.hide) return false;
|
||
|
||
for (i = 0; i < length; i++) {
|
||
x = data[i][0];
|
||
y = data[i][1];
|
||
if (x !== 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('<div style="', divStyle, '" class="flotr-grid-label">', label, '</div>');
|
||
}
|
||
else {
|
||
style.textAlign = textAlign;
|
||
style.textBaseline = textBaseline;
|
||
Flotr.drawText(context, label, distX, distY, style);
|
||
}
|
||
}
|
||
|
||
if (options.htmlText || !options.textEnabled) {
|
||
var div = Flotr.DOM.node('<div style="color:' + options.fontColor + '" class="flotr-labels"></div>');
|
||
Flotr.DOM.insert(div, html.join(''));
|
||
Flotr.DOM.insert(options.element, div);
|
||
}
|
||
|
||
context.restore();
|
||
|
||
// New start angle
|
||
this.startAngle = endAngle;
|
||
this.slices = this.slices || [];
|
||
this.slices.push({
|
||
radius : Math.min(canvas.width, canvas.height) * sizeRatio / 2,
|
||
x : x,
|
||
y : y,
|
||
explode : explode,
|
||
start : startAngle,
|
||
end : endAngle
|
||
});
|
||
},
|
||
plotSlice : function (x, y, radius, startAngle, endAngle, context) {
|
||
context.beginPath();
|
||
context.moveTo(x, y);
|
||
context.arc(x, y, radius, startAngle, endAngle, false);
|
||
context.lineTo(x, y);
|
||
context.closePath();
|
||
},
|
||
hit : function (options) {
|
||
|
||
var
|
||
data = options.data[0],
|
||
args = options.args,
|
||
index = options.index,
|
||
mouse = args[0],
|
||
n = args[1],
|
||
slice = this.slices[index],
|
||
x = mouse.relX - options.width / 2,
|
||
y = mouse.relY - options.height / 2,
|
||
r = Math.sqrt(x * x + y * y),
|
||
theta = Math.atan(y / x),
|
||
circle = Math.PI * 2,
|
||
explode = slice.explode || options.explode,
|
||
start = slice.start % circle,
|
||
end = slice.end % circle,
|
||
epsilon = options.epsilon;
|
||
|
||
if (x < 0) {
|
||
theta += Math.PI;
|
||
} else if (x > 0 && y < 0) {
|
||
theta += circle;
|
||
}
|
||
|
||
if (r < slice.radius + explode && r > explode) {
|
||
if (
|
||
(theta > start && theta < end) || // Normal Slice
|
||
(start > end && (theta < end || theta > start)) || // First slice
|
||
// TODO: Document the two cases at the end:
|
||
(start === end && ((slice.start === slice.end && Math.abs(theta - start) < epsilon) || (slice.start !== slice.end && Math.abs(theta-start) > epsilon)))
|
||
) {
|
||
|
||
// TODO Decouple this from hit plugin (chart shouldn't know what n means)
|
||
n.x = data[0];
|
||
n.y = data[1];
|
||
n.sAngle = start;
|
||
n.eAngle = end;
|
||
n.index = 0;
|
||
n.seriesIndex = index;
|
||
n.fraction = data[1] / this.total;
|
||
}
|
||
}
|
||
},
|
||
drawHit: function (options) {
|
||
var
|
||
context = options.context,
|
||
slice = this.slices[options.args.seriesIndex];
|
||
|
||
context.save();
|
||
context.translate(options.width / 2, options.height / 2);
|
||
this.plotSlice(slice.x, slice.y, slice.radius, slice.start, slice.end, context);
|
||
context.stroke();
|
||
context.restore();
|
||
},
|
||
clearHit : function (options) {
|
||
var
|
||
context = options.context,
|
||
slice = this.slices[options.args.seriesIndex],
|
||
padding = 2 * options.lineWidth,
|
||
radius = slice.radius + padding;
|
||
|
||
context.save();
|
||
context.translate(options.width / 2, options.height / 2);
|
||
context.clearRect(
|
||
slice.x - radius,
|
||
slice.y - radius,
|
||
2 * radius + padding,
|
||
2 * radius + padding
|
||
);
|
||
context.restore();
|
||
},
|
||
extendYRange : function (axis, data) {
|
||
this.total = (this.total || 0) + data[0][1];
|
||
}
|
||
});
|
||
})();
|
||
|
||
/** Points **/
|
||
Flotr.addType('points', {
|
||
options: {
|
||
show: false, // => setting to true will show points, false will hide
|
||
radius: 3, // => point radius (pixels)
|
||
lineWidth: 2, // => line width in pixels
|
||
fill: true, // => true to fill the points with a color, false for (transparent) no fill
|
||
fillColor: '#FFFFFF', // => fill color. Null to use series color.
|
||
fillOpacity: 1, // => opacity of color inside the points
|
||
hitRadius: null // => override for points hit radius
|
||
},
|
||
|
||
draw : function (options) {
|
||
var
|
||
context = options.context,
|
||
lineWidth = options.lineWidth,
|
||
shadowSize = options.shadowSize;
|
||
|
||
context.save();
|
||
|
||
if (shadowSize > 0) {
|
||
context.lineWidth = shadowSize / 2;
|
||
|
||
context.strokeStyle = 'rgba(0,0,0,0.1)';
|
||
this.plot(options, shadowSize / 2 + context.lineWidth / 2);
|
||
|
||
context.strokeStyle = 'rgba(0,0,0,0.2)';
|
||
this.plot(options, context.lineWidth / 2);
|
||
}
|
||
|
||
context.lineWidth = options.lineWidth;
|
||
context.strokeStyle = options.color;
|
||
if (options.fill) context.fillStyle = options.fillStyle;
|
||
|
||
this.plot(options);
|
||
context.restore();
|
||
},
|
||
|
||
plot : function (options, offset) {
|
||
var
|
||
data = options.data,
|
||
context = options.context,
|
||
xScale = options.xScale,
|
||
yScale = options.yScale,
|
||
i, x, y;
|
||
|
||
for (i = data.length - 1; i > -1; --i) {
|
||
y = data[i][1];
|
||
if (y === null) continue;
|
||
|
||
x = xScale(data[i][0]);
|
||
y = yScale(y);
|
||
|
||
if (x < 0 || x > options.width || y < 0 || y > options.height) continue;
|
||
|
||
context.beginPath();
|
||
if (offset) {
|
||
context.arc(x, y + offset, options.radius, 0, Math.PI, false);
|
||
} else {
|
||
context.arc(x, y, options.radius, 0, 2 * Math.PI, true);
|
||
if (options.fill) context.fill();
|
||
}
|
||
context.stroke();
|
||
context.closePath();
|
||
}
|
||
}
|
||
});
|
||
|
||
/** Radar **/
|
||
Flotr.addType('radar', {
|
||
options: {
|
||
show: false, // => setting to true will show radar chart, false will hide
|
||
lineWidth: 2, // => line width in pixels
|
||
fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill
|
||
fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
|
||
radiusRatio: 0.90 // => ratio of the radar, against the plot size
|
||
},
|
||
draw : function (options) {
|
||
var
|
||
context = options.context,
|
||
shadowSize = options.shadowSize;
|
||
|
||
context.save();
|
||
context.translate(options.width / 2, options.height / 2);
|
||
context.lineWidth = options.lineWidth;
|
||
|
||
// Shadow
|
||
context.fillStyle = 'rgba(0,0,0,0.05)';
|
||
context.strokeStyle = 'rgba(0,0,0,0.05)';
|
||
this.plot(options, shadowSize / 2);
|
||
context.strokeStyle = 'rgba(0,0,0,0.1)';
|
||
this.plot(options, shadowSize / 4);
|
||
|
||
// Chart
|
||
context.strokeStyle = options.color;
|
||
context.fillStyle = options.fillStyle;
|
||
this.plot(options);
|
||
|
||
context.restore();
|
||
},
|
||
plot : function (options, offset) {
|
||
var
|
||
data = options.data,
|
||
context = options.context,
|
||
radius = Math.min(options.height, options.width) * options.radiusRatio / 2,
|
||
step = 2 * Math.PI / data.length,
|
||
angle = -Math.PI / 2,
|
||
i, ratio;
|
||
|
||
offset = offset || 0;
|
||
|
||
context.beginPath();
|
||
for (i = 0; i < data.length; ++i) {
|
||
ratio = data[i][1] / this.max;
|
||
|
||
context[i === 0 ? 'moveTo' : 'lineTo'](
|
||
Math.cos(i * step + angle) * radius * ratio + offset,
|
||
Math.sin(i * step + angle) * radius * ratio + offset
|
||
);
|
||
}
|
||
context.closePath();
|
||
if (options.fill) context.fill();
|
||
context.stroke();
|
||
},
|
||
extendYRange : function (axis, data) {
|
||
this.max = Math.max(axis.max, this.max || -Number.MAX_VALUE);
|
||
}
|
||
});
|
||
|
||
Flotr.addType('timeline', {
|
||
options: {
|
||
show: false,
|
||
lineWidth: 1,
|
||
barWidth: 0.2,
|
||
fill: true,
|
||
fillColor: null,
|
||
fillOpacity: 0.4,
|
||
centered: true
|
||
},
|
||
|
||
draw : function (options) {
|
||
|
||
var
|
||
context = options.context;
|
||
|
||
context.save();
|
||
context.lineJoin = 'miter';
|
||
context.lineWidth = options.lineWidth;
|
||
context.strokeStyle = options.color;
|
||
context.fillStyle = options.fillStyle;
|
||
|
||
this.plot(options);
|
||
|
||
context.restore();
|
||
},
|
||
|
||
plot : function (options) {
|
||
|
||
var
|
||
data = options.data,
|
||
context = options.context,
|
||
xScale = options.xScale,
|
||
yScale = options.yScale,
|
||
barWidth = options.barWidth,
|
||
lineWidth = options.lineWidth,
|
||
i;
|
||
|
||
Flotr._.each(data, function (timeline) {
|
||
|
||
var
|
||
x = timeline[0],
|
||
y = timeline[1],
|
||
w = timeline[2],
|
||
h = barWidth,
|
||
|
||
xt = Math.ceil(xScale(x)),
|
||
wt = Math.ceil(xScale(x + w)) - xt,
|
||
yt = Math.round(yScale(y)),
|
||
ht = Math.round(yScale(y - h)) - yt,
|
||
|
||
x0 = xt - lineWidth / 2,
|
||
y0 = Math.round(yt - ht / 2) - lineWidth / 2;
|
||
|
||
context.strokeRect(x0, y0, wt, ht);
|
||
context.fillRect(x0, y0, wt, ht);
|
||
|
||
});
|
||
},
|
||
|
||
extendRange : function (series) {
|
||
|
||
var
|
||
data = series.data,
|
||
xa = series.xaxis,
|
||
ya = series.yaxis,
|
||
w = series.timeline.barWidth;
|
||
|
||
if (xa.options.min === null)
|
||
xa.min = xa.datamin - w / 2;
|
||
|
||
if (xa.options.max === null) {
|
||
|
||
var
|
||
max = xa.max;
|
||
|
||
Flotr._.each(data, function (timeline) {
|
||
max = Math.max(max, timeline[0] + timeline[2]);
|
||
}, this);
|
||
|
||
xa.max = max + w / 2;
|
||
}
|
||
|
||
if (ya.options.min === null)
|
||
ya.min = ya.datamin - w;
|
||
if (ya.options.min === null)
|
||
ya.max = ya.datamax + w;
|
||
}
|
||
|
||
});
|
||
|
||
(function () {
|
||
|
||
var D = Flotr.DOM;
|
||
|
||
Flotr.addPlugin('crosshair', {
|
||
options: {
|
||
mode: null, // => one of null, 'x', 'y' or 'xy'
|
||
color: '#FF0000', // => crosshair color
|
||
hideCursor: true // => hide the cursor when the crosshair is shown
|
||
},
|
||
callbacks: {
|
||
'flotr:mousemove': function(e, pos) {
|
||
if (this.options.crosshair.mode) {
|
||
this.crosshair.clearCrosshair();
|
||
this.crosshair.drawCrosshair(pos);
|
||
}
|
||
}
|
||
},
|
||
/**
|
||
* Draws the selection box.
|
||
*/
|
||
drawCrosshair: function(pos) {
|
||
var octx = this.octx,
|
||
options = this.options.crosshair,
|
||
plotOffset = this.plotOffset,
|
||
x = plotOffset.left + Math.round(pos.relX) + 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 = '<html><body>'+this.canvas.firstChild.innerHTML+'</body></html>';
|
||
return window.open().document.write(image);
|
||
}
|
||
|
||
if (type !== 'jpeg' && type !== 'png') return;
|
||
|
||
image = getImage(type, this.canvas, width, height);
|
||
|
||
if (_.isElement(image) && replaceCanvas) {
|
||
this.download.restoreCanvas();
|
||
D.hide(this.canvas);
|
||
D.hide(this.overlay);
|
||
D.setStyles({position: 'absolute'});
|
||
D.insert(this.el, image);
|
||
this.saveImageElement = image;
|
||
} else {
|
||
return window.open(image.src);
|
||
}
|
||
},
|
||
|
||
restoreCanvas: function() {
|
||
D.show(this.canvas);
|
||
D.show(this.overlay);
|
||
if (this.saveImageElement) this.el.removeChild(this.saveImageElement);
|
||
this.saveImageElement = null;
|
||
}
|
||
});
|
||
|
||
})();
|
||
|
||
(function () {
|
||
|
||
var E = Flotr.EventAdapter,
|
||
_ = Flotr._;
|
||
|
||
Flotr.addPlugin('graphGrid', {
|
||
|
||
callbacks: {
|
||
'flotr:beforedraw' : function () {
|
||
this.graphGrid.drawGrid();
|
||
},
|
||
'flotr:afterdraw' : function () {
|
||
this.graphGrid.drawOutline();
|
||
}
|
||
},
|
||
|
||
drawGrid: function(){
|
||
|
||
var
|
||
ctx = this.ctx,
|
||
options = this.options,
|
||
grid = options.grid,
|
||
verticalLines = grid.verticalLines,
|
||
horizontalLines = grid.horizontalLines,
|
||
minorVerticalLines = grid.minorVerticalLines,
|
||
minorHorizontalLines = grid.minorHorizontalLines,
|
||
plotHeight = this.plotHeight,
|
||
plotWidth = this.plotWidth,
|
||
a, v, i, j;
|
||
|
||
if(verticalLines || minorVerticalLines ||
|
||
horizontalLines || minorHorizontalLines){
|
||
E.fire(this.el, 'flotr:beforegrid', [this.axes.x, this.axes.y, options, this]);
|
||
}
|
||
ctx.save();
|
||
ctx.lineWidth = 1;
|
||
ctx.strokeStyle = grid.tickColor;
|
||
|
||
function circularHorizontalTicks (ticks) {
|
||
for(i = 0; i < ticks.length; ++i){
|
||
var ratio = ticks[i].v / a.max;
|
||
for(j = 0; j <= sides; ++j){
|
||
ctx[j === 0 ? 'moveTo' : 'lineTo'](
|
||
Math.cos(j*coeff+angle)*radius*ratio,
|
||
Math.sin(j*coeff+angle)*radius*ratio
|
||
);
|
||
}
|
||
}
|
||
}
|
||
function drawGridLines (ticks, callback) {
|
||
_.each(_.pluck(ticks, 'v'), function(v){
|
||
// Don't show lines on upper and lower bounds.
|
||
if ((v <= a.min || v >= a.max) ||
|
||
(v == a.min || v == a.max) && grid.outlineWidth)
|
||
return;
|
||
callback(Math.floor(a.d2p(v)) + ctx.lineWidth/2);
|
||
});
|
||
}
|
||
function drawVerticalLines (x) {
|
||
ctx.moveTo(x, 0);
|
||
ctx.lineTo(x, plotHeight);
|
||
}
|
||
function drawHorizontalLines (y) {
|
||
ctx.moveTo(0, y);
|
||
ctx.lineTo(plotWidth, y);
|
||
}
|
||
|
||
if (grid.circular) {
|
||
ctx.translate(this.plotOffset.left+plotWidth/2, this.plotOffset.top+plotHeight/2);
|
||
var radius = Math.min(plotHeight, plotWidth)*options.radar.radiusRatio/2,
|
||
sides = this.axes.x.ticks.length,
|
||
coeff = 2*(Math.PI/sides),
|
||
angle = -Math.PI/2;
|
||
|
||
// Draw grid lines in vertical direction.
|
||
ctx.beginPath();
|
||
|
||
a = this.axes.y;
|
||
|
||
if(horizontalLines){
|
||
circularHorizontalTicks(a.ticks);
|
||
}
|
||
if(minorHorizontalLines){
|
||
circularHorizontalTicks(a.minorTicks);
|
||
}
|
||
|
||
if(verticalLines){
|
||
_.times(sides, function(i){
|
||
ctx.moveTo(0, 0);
|
||
ctx.lineTo(Math.cos(i*coeff+angle)*radius, Math.sin(i*coeff+angle)*radius);
|
||
});
|
||
}
|
||
ctx.stroke();
|
||
}
|
||
else {
|
||
ctx.translate(this.plotOffset.left, this.plotOffset.top);
|
||
|
||
// Draw grid background, if present in options.
|
||
if(grid.backgroundColor){
|
||
ctx.fillStyle = this.processColor(grid.backgroundColor, {x1: 0, y1: 0, x2: plotWidth, y2: plotHeight});
|
||
ctx.fillRect(0, 0, plotWidth, plotHeight);
|
||
}
|
||
|
||
ctx.beginPath();
|
||
|
||
a = this.axes.x;
|
||
if (verticalLines) drawGridLines(a.ticks, drawVerticalLines);
|
||
if (minorVerticalLines) drawGridLines(a.minorTicks, drawVerticalLines);
|
||
|
||
a = this.axes.y;
|
||
if (horizontalLines) drawGridLines(a.ticks, drawHorizontalLines);
|
||
if (minorHorizontalLines) drawGridLines(a.minorTicks, drawHorizontalLines);
|
||
|
||
ctx.stroke();
|
||
}
|
||
|
||
ctx.restore();
|
||
if(verticalLines || minorVerticalLines ||
|
||
horizontalLines || minorHorizontalLines){
|
||
E.fire(this.el, 'flotr:aftergrid', [this.axes.x, this.axes.y, options, this]);
|
||
}
|
||
},
|
||
|
||
drawOutline: function(){
|
||
var
|
||
that = this,
|
||
options = that.options,
|
||
grid = options.grid,
|
||
outline = grid.outline,
|
||
ctx = that.ctx,
|
||
backgroundImage = grid.backgroundImage,
|
||
plotOffset = that.plotOffset,
|
||
leftOffset = plotOffset.left,
|
||
topOffset = plotOffset.top,
|
||
plotWidth = that.plotWidth,
|
||
plotHeight = that.plotHeight,
|
||
v, img, src, left, top, globalAlpha;
|
||
|
||
if (!grid.outlineWidth) return;
|
||
|
||
ctx.save();
|
||
|
||
if (grid.circular) {
|
||
ctx.translate(leftOffset + plotWidth / 2, topOffset + plotHeight / 2);
|
||
var radius = Math.min(plotHeight, plotWidth) * options.radar.radiusRatio / 2,
|
||
sides = this.axes.x.ticks.length,
|
||
coeff = 2*(Math.PI/sides),
|
||
angle = -Math.PI/2;
|
||
|
||
// Draw axis/grid border.
|
||
ctx.beginPath();
|
||
ctx.lineWidth = grid.outlineWidth;
|
||
ctx.strokeStyle = grid.color;
|
||
ctx.lineJoin = 'round';
|
||
|
||
for(i = 0; i <= sides; ++i){
|
||
ctx[i === 0 ? 'moveTo' : 'lineTo'](Math.cos(i*coeff+angle)*radius, Math.sin(i*coeff+angle)*radius);
|
||
}
|
||
//ctx.arc(0, 0, radius, 0, Math.PI*2, true);
|
||
|
||
ctx.stroke();
|
||
}
|
||
else {
|
||
ctx.translate(leftOffset, topOffset);
|
||
|
||
// Draw axis/grid border.
|
||
var lw = grid.outlineWidth,
|
||
orig = 0.5-lw+((lw+1)%2/2),
|
||
lineTo = 'lineTo',
|
||
moveTo = 'moveTo';
|
||
ctx.lineWidth = lw;
|
||
ctx.strokeStyle = grid.color;
|
||
ctx.lineJoin = 'miter';
|
||
ctx.beginPath();
|
||
ctx.moveTo(orig, orig);
|
||
plotWidth = plotWidth - (lw / 2) % 1;
|
||
plotHeight = plotHeight + lw / 2;
|
||
ctx[outline.indexOf('n') !== -1 ? lineTo : moveTo](plotWidth, orig);
|
||
ctx[outline.indexOf('e') !== -1 ? lineTo : moveTo](plotWidth, plotHeight);
|
||
ctx[outline.indexOf('s') !== -1 ? lineTo : moveTo](orig, plotHeight);
|
||
ctx[outline.indexOf('w') !== -1 ? lineTo : moveTo](orig, orig);
|
||
ctx.stroke();
|
||
ctx.closePath();
|
||
}
|
||
|
||
ctx.restore();
|
||
|
||
if (backgroundImage) {
|
||
|
||
src = backgroundImage.src || backgroundImage;
|
||
left = (parseInt(backgroundImage.left, 10) || 0) + plotOffset.left;
|
||
top = (parseInt(backgroundImage.top, 10) || 0) + plotOffset.top;
|
||
img = new Image();
|
||
|
||
img.onload = function() {
|
||
ctx.save();
|
||
if (backgroundImage.alpha) ctx.globalAlpha = backgroundImage.alpha;
|
||
ctx.globalCompositeOperation = 'destination-over';
|
||
ctx.drawImage(img, 0, 0, img.width, img.height, left, top, plotWidth, plotHeight);
|
||
ctx.restore();
|
||
};
|
||
|
||
img.src = src;
|
||
}
|
||
}
|
||
});
|
||
|
||
})();
|
||
|
||
(function () {
|
||
|
||
var
|
||
D = Flotr.DOM,
|
||
_ = Flotr._,
|
||
flotr = Flotr,
|
||
S_MOUSETRACK = 'opacity:0.7;background-color:#000;color:#fff;display:none;position:absolute;padding:2px 8px;-moz-border-radius:4px;border-radius:4px;white-space:nowrap;';
|
||
|
||
Flotr.addPlugin('hit', {
|
||
callbacks: {
|
||
'flotr:mousemove': function(e, pos) {
|
||
this.hit.track(pos);
|
||
},
|
||
'flotr:click': function(pos) {
|
||
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('<div class="flotr-mouse-value"></div>');
|
||
this.mouseTrack = mouseTrack;
|
||
D.insert(this.el, mouseTrack);
|
||
}
|
||
|
||
if (!n.mouse.relative) { // absolute to the canvas
|
||
|
||
if (p.charAt(0) == 'n') pos += 'top:' + (m + top) + 'px;bottom:auto;';
|
||
else if (p.charAt(0) == 's') pos += 'bottom:' + (m + bottom) + 'px;top:auto;';
|
||
if (p.charAt(1) == 'e') pos += 'right:' + (m + right) + 'px;left:auto;';
|
||
else if (p.charAt(1) == 'w') pos += 'left:' + (m + left) + 'px;right:auto;';
|
||
|
||
// Pie
|
||
} else if (s.pie && s.pie.show) {
|
||
var center = {
|
||
x: (this.plotWidth)/2,
|
||
y: (this.plotHeight)/2
|
||
},
|
||
radius = (Math.min(this.canvasWidth, this.canvasHeight) * s.pie.sizeRatio) / 2,
|
||
bisection = n.sAngle<n.eAngle ? (n.sAngle + n.eAngle) / 2: (n.sAngle + n.eAngle + 2* Math.PI) / 2;
|
||
|
||
pos += 'bottom:' + (m - top - center.y - Math.sin(bisection) * radius/2 + this.canvasHeight) + 'px;top:auto;';
|
||
pos += 'left:' + (m + left + center.x + Math.cos(bisection) * radius/2) + 'px;right:auto;';
|
||
|
||
// Default
|
||
} else {
|
||
if (/n/.test(p)) pos += 'bottom:' + (m - top - n.yaxis.d2p(n.y) + this.canvasHeight) + 'px;top:auto;';
|
||
else pos += 'top:' + (m + top + n.yaxis.d2p(n.y)) + 'px;bottom:auto;';
|
||
if (/w/.test(p)) pos += 'right:' + (m - left - n.xaxis.d2p(n.x) + this.canvasWidth) + 'px;left:auto;';
|
||
else pos += 'left:' + (m + left + n.xaxis.d2p(n.x)) + 'px;right:auto;';
|
||
}
|
||
|
||
elStyle += pos;
|
||
mouseTrack.style.cssText = elStyle;
|
||
if (!decimals || decimals < 0) decimals = 0;
|
||
|
||
if (x && x.toFixed) x = x.toFixed(decimals);
|
||
|
||
if (y && y.toFixed) y = y.toFixed(decimals);
|
||
|
||
mouseTrack.innerHTML = n.mouse.trackFormatter({
|
||
x: x ,
|
||
y: y,
|
||
series: n.series,
|
||
index: n.index,
|
||
nearest: n,
|
||
fraction: n.fraction
|
||
});
|
||
|
||
D.show(mouseTrack);
|
||
|
||
if (n.mouse.relative) {
|
||
if (!/[ew]/.test(p)) {
|
||
// Center Horizontally
|
||
mouseTrack.style.left =
|
||
(left + n.xaxis.d2p(n.x) - D.size(mouseTrack).width / 2) + 'px';
|
||
} else
|
||
if (!/[ns]/.test(p)) {
|
||
// Center Vertically
|
||
mouseTrack.style.top =
|
||
(top + n.yaxis.d2p(n.y) - D.size(mouseTrack).height / 2) + 'px';
|
||
}
|
||
}
|
||
}
|
||
|
||
});
|
||
})();
|
||
|
||
/**
|
||
* Selection Handles Plugin
|
||
*
|
||
*
|
||
* Options
|
||
* show - True enables the handles plugin.
|
||
* drag - Left and Right drag handles
|
||
* scroll - Scrolling handle
|
||
*/
|
||
(function () {
|
||
|
||
function isLeftClick (e, type) {
|
||
return (e.which ? (e.which === 1) : (e.button === 0 || e.button === 1));
|
||
}
|
||
|
||
function boundX(x, graph) {
|
||
return Math.min(Math.max(0, x), graph.plotWidth - 1);
|
||
}
|
||
|
||
function boundY(y, graph) {
|
||
return Math.min(Math.max(0, y), graph.plotHeight);
|
||
}
|
||
|
||
var
|
||
D = Flotr.DOM,
|
||
E = Flotr.EventAdapter,
|
||
_ = Flotr._;
|
||
|
||
|
||
Flotr.addPlugin('selection', {
|
||
|
||
options: {
|
||
pinchOnly: null, // Only select on pinch
|
||
mode: null, // => one of null, 'x', 'y' or 'xy'
|
||
color: '#B6D9FF', // => selection box color
|
||
fps: 20 // => frames-per-second
|
||
},
|
||
|
||
callbacks: {
|
||
'flotr:mouseup' : function (event) {
|
||
|
||
var
|
||
options = this.options.selection,
|
||
selection = this.selection,
|
||
pointer = this.getEventPosition(event);
|
||
|
||
if (!options || !options.mode) return;
|
||
if (selection.interval) clearInterval(selection.interval);
|
||
|
||
if (this.multitouches) {
|
||
selection.updateSelection();
|
||
} else
|
||
if (!options.pinchOnly) {
|
||
selection.setSelectionPos(selection.selection.second, pointer);
|
||
}
|
||
selection.clearSelection();
|
||
|
||
if(selection.selecting && selection.selectionIsSane()){
|
||
selection.drawSelection();
|
||
selection.fireSelectEvent();
|
||
this.ignoreClick = true;
|
||
}
|
||
},
|
||
'flotr:mousedown' : function (event) {
|
||
|
||
var
|
||
options = this.options.selection,
|
||
selection = this.selection,
|
||
pointer = this.getEventPosition(event);
|
||
|
||
if (!options || !options.mode) return;
|
||
if (!options.mode || (!isLeftClick(event) && _.isUndefined(event.touches))) return;
|
||
if (!options.pinchOnly) selection.setSelectionPos(selection.selection.first, pointer);
|
||
if (selection.interval) clearInterval(selection.interval);
|
||
|
||
this.lastMousePos.pageX = null;
|
||
selection.selecting = false;
|
||
selection.interval = setInterval(
|
||
_.bind(selection.updateSelection, this),
|
||
1000 / options.fps
|
||
);
|
||
},
|
||
'flotr:destroy' : function (event) {
|
||
clearInterval(this.selection.interval);
|
||
}
|
||
},
|
||
|
||
// TODO This isn't used. Maybe it belongs in the draw area and fire select event methods?
|
||
getArea: function() {
|
||
|
||
var
|
||
s = this.selection.selection,
|
||
a = this.axes,
|
||
first = s.first,
|
||
second = s.second,
|
||
x1, x2, y1, y2;
|
||
|
||
x1 = a.x.p2d(s.first.x);
|
||
x2 = a.x.p2d(s.second.x);
|
||
y1 = a.y.p2d(s.first.y);
|
||
y2 = a.y.p2d(s.second.y);
|
||
|
||
return {
|
||
x1 : Math.min(x1, x2),
|
||
y1 : Math.min(y1, y2),
|
||
x2 : Math.max(x1, x2),
|
||
y2 : Math.max(y1, y2),
|
||
xfirst : x1,
|
||
xsecond : x2,
|
||
yfirst : y1,
|
||
ysecond : y2
|
||
};
|
||
},
|
||
|
||
selection: {first: {x: -1, y: -1}, second: {x: -1, y: -1}},
|
||
prevSelection: null,
|
||
interval: null,
|
||
|
||
/**
|
||
* Fires the 'flotr:select' event when the user made a selection.
|
||
*/
|
||
fireSelectEvent: function(name){
|
||
var
|
||
area = this.selection.getArea();
|
||
name = name || 'select';
|
||
area.selection = this.selection.selection;
|
||
E.fire(this.el, 'flotr:'+name, [area, this]);
|
||
},
|
||
|
||
/**
|
||
* Allows the user the manually select an area.
|
||
* @param {Object} area - Object with coordinates to select.
|
||
*/
|
||
setSelection: function(area, preventEvent){
|
||
var options = this.options,
|
||
xa = this.axes.x,
|
||
ya = this.axes.y,
|
||
vertScale = ya.scale,
|
||
hozScale = xa.scale,
|
||
selX = options.selection.mode.indexOf('x') != -1,
|
||
selY = options.selection.mode.indexOf('y') != -1,
|
||
s = this.selection.selection;
|
||
|
||
this.selection.clearSelection();
|
||
|
||
s.first.y = boundY((selX && !selY) ? 0 : (ya.max - area.y1) * vertScale, this);
|
||
s.second.y = boundY((selX && !selY) ? this.plotHeight - 1: (ya.max - area.y2) * vertScale, this);
|
||
s.first.x = boundX((selY && !selX) ? 0 : (area.x1 - xa.min) * hozScale, this);
|
||
s.second.x = boundX((selY && !selX) ? this.plotWidth : (area.x2 - xa.min) * hozScale, this);
|
||
|
||
this.selection.drawSelection();
|
||
if (!preventEvent)
|
||
this.selection.fireSelectEvent();
|
||
},
|
||
|
||
/**
|
||
* Calculates the position of the selection.
|
||
* @param {Object} pos - Position object.
|
||
* @param {Event} event - Event object.
|
||
*/
|
||
setSelectionPos: function(pos, pointer) {
|
||
var mode = this.options.selection.mode,
|
||
selection = this.selection.selection;
|
||
|
||
if(mode.indexOf('x') == -1) {
|
||
pos.x = (pos == selection.first) ? 0 : this.plotWidth;
|
||
}else{
|
||
pos.x = boundX(pointer.relX, this);
|
||
}
|
||
|
||
if (mode.indexOf('y') == -1) {
|
||
pos.y = (pos == selection.first) ? 0 : this.plotHeight - 1;
|
||
}else{
|
||
pos.y = boundY(pointer.relY, this);
|
||
}
|
||
},
|
||
/**
|
||
* Draws the selection box.
|
||
*/
|
||
drawSelection: function() {
|
||
|
||
this.selection.fireSelectEvent('selecting');
|
||
|
||
var s = this.selection.selection,
|
||
octx = this.octx,
|
||
options = this.options,
|
||
plotOffset = this.plotOffset,
|
||
prevSelection = this.selection.prevSelection;
|
||
|
||
if (prevSelection &&
|
||
s.first.x == prevSelection.first.x &&
|
||
s.first.y == prevSelection.first.y &&
|
||
s.second.x == prevSelection.second.x &&
|
||
s.second.y == prevSelection.second.y) {
|
||
return;
|
||
}
|
||
|
||
octx.save();
|
||
octx.strokeStyle = this.processColor(options.selection.color, {opacity: 0.8});
|
||
octx.lineWidth = 1;
|
||
octx.lineJoin = 'miter';
|
||
octx.fillStyle = this.processColor(options.selection.color, {opacity: 0.4});
|
||
|
||
this.selection.prevSelection = {
|
||
first: { x: s.first.x, y: s.first.y },
|
||
second: { x: s.second.x, y: s.second.y }
|
||
};
|
||
|
||
var x = Math.min(s.first.x, s.second.x),
|
||
y = Math.min(s.first.y, s.second.y),
|
||
w = Math.abs(s.second.x - s.first.x),
|
||
h = Math.abs(s.second.y - s.first.y);
|
||
|
||
octx.fillRect(x + plotOffset.left+0.5, y + plotOffset.top+0.5, w, h);
|
||
octx.strokeRect(x + plotOffset.left+0.5, y + plotOffset.top+0.5, w, h);
|
||
octx.restore();
|
||
},
|
||
|
||
/**
|
||
* Updates (draws) the selection box.
|
||
*/
|
||
updateSelection: function(){
|
||
if (!this.lastMousePos.pageX) return;
|
||
|
||
this.selection.selecting = true;
|
||
|
||
if (this.multitouches) {
|
||
this.selection.setSelectionPos(this.selection.selection.first, this.getEventPosition(this.multitouches[0]));
|
||
this.selection.setSelectionPos(this.selection.selection.second, this.getEventPosition(this.multitouches[1]));
|
||
} else
|
||
if (this.options.selection.pinchOnly) {
|
||
return;
|
||
} else {
|
||
this.selection.setSelectionPos(this.selection.selection.second, this.lastMousePos);
|
||
}
|
||
|
||
this.selection.clearSelection();
|
||
|
||
if(this.selection.selectionIsSane()) {
|
||
this.selection.drawSelection();
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Removes the selection box from the overlay canvas.
|
||
*/
|
||
clearSelection: function() {
|
||
if (!this.selection.prevSelection) return;
|
||
|
||
var prevSelection = this.selection.prevSelection,
|
||
lw = 1,
|
||
plotOffset = this.plotOffset,
|
||
x = Math.min(prevSelection.first.x, prevSelection.second.x),
|
||
y = Math.min(prevSelection.first.y, prevSelection.second.y),
|
||
w = Math.abs(prevSelection.second.x - prevSelection.first.x),
|
||
h = Math.abs(prevSelection.second.y - prevSelection.first.y);
|
||
|
||
this.octx.clearRect(x + plotOffset.left - lw + 0.5,
|
||
y + plotOffset.top - lw,
|
||
w + 2 * lw + 0.5,
|
||
h + 2 * lw + 0.5);
|
||
|
||
this.selection.prevSelection = null;
|
||
},
|
||
/**
|
||
* Determines whether or not the selection is sane and should be drawn.
|
||
* @return {Boolean} - True when sane, false otherwise.
|
||
*/
|
||
selectionIsSane: function(){
|
||
var s = this.selection.selection;
|
||
return Math.abs(s.second.x - s.first.x) >= 5 ||
|
||
Math.abs(s.second.y - s.first.y) >= 5;
|
||
}
|
||
|
||
});
|
||
|
||
})();
|
||
|
||
(function () {
|
||
|
||
var D = Flotr.DOM;
|
||
|
||
Flotr.addPlugin('labels', {
|
||
|
||
callbacks : {
|
||
'flotr:afterdraw' : function () {
|
||
this.labels.draw();
|
||
}
|
||
},
|
||
|
||
draw: function(){
|
||
// Construct fixed width label boxes, which can be styled easily.
|
||
var
|
||
axis, tick, left, top, xBoxWidth,
|
||
radius, sides, coeff, angle,
|
||
div, i, html = '',
|
||
noLabels = 0,
|
||
options = this.options,
|
||
ctx = this.ctx,
|
||
a = this.axes,
|
||
style = { size: options.fontSize };
|
||
|
||
for (i = 0; i < a.x.ticks.length; ++i){
|
||
if (a.x.ticks[i].label) { ++noLabels; }
|
||
}
|
||
xBoxWidth = this.plotWidth / noLabels;
|
||
|
||
if (options.grid.circular) {
|
||
ctx.save();
|
||
ctx.translate(this.plotOffset.left + this.plotWidth / 2,
|
||
this.plotOffset.top + this.plotHeight / 2);
|
||
|
||
radius = this.plotHeight * options.radar.radiusRatio / 2 + options.fontSize;
|
||
sides = this.axes.x.ticks.length;
|
||
coeff = 2 * (Math.PI / sides);
|
||
angle = -Math.PI / 2;
|
||
|
||
drawLabelCircular(this, a.x, false);
|
||
drawLabelCircular(this, a.x, true);
|
||
drawLabelCircular(this, a.y, false);
|
||
drawLabelCircular(this, a.y, true);
|
||
ctx.restore();
|
||
}
|
||
|
||
if (!options.HtmlText && this.textEnabled) {
|
||
drawLabelNoHtmlText(this, a.x, 'center', 'top');
|
||
drawLabelNoHtmlText(this, a.x2, 'center', 'bottom');
|
||
drawLabelNoHtmlText(this, a.y, 'right', 'middle');
|
||
drawLabelNoHtmlText(this, a.y2, 'left', 'middle');
|
||
|
||
} else if ((
|
||
a.x.options.showLabels ||
|
||
a.x2.options.showLabels ||
|
||
a.y.options.showLabels ||
|
||
a.y2.options.showLabels) &&
|
||
!options.grid.circular
|
||
) {
|
||
|
||
html = '';
|
||
|
||
drawLabelHtml(this, a.x);
|
||
drawLabelHtml(this, a.x2);
|
||
drawLabelHtml(this, a.y);
|
||
drawLabelHtml(this, a.y2);
|
||
|
||
ctx.stroke();
|
||
ctx.restore();
|
||
div = D.create('div');
|
||
D.setStyles(div, {
|
||
fontSize: 'smaller',
|
||
color: options.grid.color
|
||
});
|
||
div.className = 'flotr-labels';
|
||
D.insert(this.el, div);
|
||
D.insert(div, html);
|
||
}
|
||
|
||
function drawLabelCircular (graph, axis, minorTicks) {
|
||
var
|
||
ticks = minorTicks ? axis.minorTicks : axis.ticks,
|
||
isX = axis.orientation === 1,
|
||
isFirst = axis.n === 1,
|
||
style, offset;
|
||
|
||
style = {
|
||
color : axis.options.color || options.grid.color,
|
||
angle : Flotr.toRad(axis.options.labelsAngle),
|
||
textBaseline : 'middle'
|
||
};
|
||
|
||
for (i = 0; i < ticks.length &&
|
||
(minorTicks ? axis.options.showMinorLabels : axis.options.showLabels); ++i){
|
||
tick = ticks[i];
|
||
tick.label += '';
|
||
if (!tick.label || !tick.label.length) { continue; }
|
||
|
||
x = Math.cos(i * coeff + angle) * radius;
|
||
y = Math.sin(i * coeff + angle) * radius;
|
||
|
||
style.textAlign = isX ? (Math.abs(x) < 0.1 ? 'center' : (x < 0 ? 'right' : 'left')) : 'left';
|
||
|
||
Flotr.drawText(
|
||
ctx, tick.label,
|
||
isX ? x : 3,
|
||
isX ? y : -(axis.ticks[i].v / axis.max) * (radius - options.fontSize),
|
||
style
|
||
);
|
||
}
|
||
}
|
||
|
||
function drawLabelNoHtmlText (graph, axis, textAlign, textBaseline) {
|
||
var
|
||
isX = axis.orientation === 1,
|
||
isFirst = axis.n === 1,
|
||
style, offset;
|
||
|
||
style = {
|
||
color : axis.options.color || options.grid.color,
|
||
textAlign : textAlign,
|
||
textBaseline : textBaseline,
|
||
angle : Flotr.toRad(axis.options.labelsAngle)
|
||
};
|
||
style = Flotr.getBestTextAlign(style.angle, style);
|
||
|
||
for (i = 0; i < axis.ticks.length && continueShowingLabels(axis); ++i) {
|
||
|
||
tick = axis.ticks[i];
|
||
if (!tick.label || !tick.label.length) { continue; }
|
||
|
||
offset = axis.d2p(tick.v);
|
||
if (offset < 0 ||
|
||
offset > (isX ? graph.plotWidth : graph.plotHeight)) { continue; }
|
||
|
||
Flotr.drawText(
|
||
ctx, tick.label,
|
||
leftOffset(graph, isX, isFirst, offset),
|
||
topOffset(graph, isX, isFirst, offset),
|
||
style
|
||
);
|
||
|
||
// Only draw on axis y2
|
||
if (!isX && !isFirst) {
|
||
ctx.save();
|
||
ctx.strokeStyle = style.color;
|
||
ctx.beginPath();
|
||
ctx.moveTo(graph.plotOffset.left + graph.plotWidth - 8, graph.plotOffset.top + axis.d2p(tick.v));
|
||
ctx.lineTo(graph.plotOffset.left + graph.plotWidth, graph.plotOffset.top + axis.d2p(tick.v));
|
||
ctx.stroke();
|
||
ctx.restore();
|
||
}
|
||
}
|
||
|
||
function continueShowingLabels (axis) {
|
||
return axis.options.showLabels && axis.used;
|
||
}
|
||
function leftOffset (graph, isX, isFirst, offset) {
|
||
return graph.plotOffset.left +
|
||
(isX ? offset :
|
||
(isFirst ?
|
||
-options.grid.labelMargin :
|
||
options.grid.labelMargin + graph.plotWidth));
|
||
}
|
||
function topOffset (graph, isX, isFirst, offset) {
|
||
return graph.plotOffset.top +
|
||
(isX ? options.grid.labelMargin : offset) +
|
||
((isX && isFirst) ? graph.plotHeight : 0);
|
||
}
|
||
}
|
||
|
||
function drawLabelHtml (graph, axis) {
|
||
var
|
||
isX = axis.orientation === 1,
|
||
isFirst = axis.n === 1,
|
||
name = '',
|
||
left, style, top,
|
||
offset = graph.plotOffset;
|
||
|
||
if (!isX && !isFirst) {
|
||
ctx.save();
|
||
ctx.strokeStyle = axis.options.color || options.grid.color;
|
||
ctx.beginPath();
|
||
}
|
||
|
||
if (axis.options.showLabels && (isFirst ? true : axis.used)) {
|
||
for (i = 0; i < axis.ticks.length; ++i) {
|
||
tick = axis.ticks[i];
|
||
if (!tick.label || !tick.label.length ||
|
||
((isX ? offset.left : offset.top) + axis.d2p(tick.v) < 0) ||
|
||
((isX ? offset.left : offset.top) + axis.d2p(tick.v) > (isX ? graph.canvasWidth : graph.canvasHeight))) {
|
||
continue;
|
||
}
|
||
top = offset.top +
|
||
(isX ?
|
||
((isFirst ? 1 : -1 ) * (graph.plotHeight + options.grid.labelMargin)) :
|
||
axis.d2p(tick.v) - axis.maxLabel.height / 2);
|
||
left = isX ? (offset.left + axis.d2p(tick.v) - xBoxWidth / 2) : 0;
|
||
|
||
name = '';
|
||
if (i === 0) {
|
||
name = ' first';
|
||
} else if (i === axis.ticks.length - 1) {
|
||
name = ' last';
|
||
}
|
||
name += isX ? ' flotr-grid-label-x' : ' flotr-grid-label-y';
|
||
|
||
html += [
|
||
'<div style="position:absolute; text-align:' + (isX ? 'center' : 'right') + '; ',
|
||
'top:' + top + 'px; ',
|
||
((!isX && !isFirst) ? 'right:' : 'left:') + left + 'px; ',
|
||
'width:' + (isX ? xBoxWidth : ((isFirst ? offset.left : offset.right) - options.grid.labelMargin)) + 'px; ',
|
||
axis.options.color ? ('color:' + axis.options.color + '; ') : ' ',
|
||
'" class="flotr-grid-label' + name + '">' + tick.label + '</div>'
|
||
].join(' ');
|
||
|
||
if (!isX && !isFirst) {
|
||
ctx.moveTo(offset.left + graph.plotWidth - 8, offset.top + axis.d2p(tick.v));
|
||
ctx.lineTo(offset.left + graph.plotWidth, offset.top + axis.d2p(tick.v));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
});
|
||
})();
|
||
|
||
(function () {
|
||
|
||
var
|
||
D = Flotr.DOM,
|
||
_ = Flotr._;
|
||
|
||
Flotr.addPlugin('legend', {
|
||
options: {
|
||
show: true, // => setting to true will show the legend, hide otherwise
|
||
noColumns: 1, // => number of colums in legend table // @todo: doesn't work for HtmlText = false
|
||
labelFormatter: function(v){return v;}, // => fn: string -> string
|
||
labelBoxBorderColor: '#CCCCCC', // => border color for the little label boxes
|
||
labelBoxWidth: 14,
|
||
labelBoxHeight: 10,
|
||
labelBoxMargin: 5,
|
||
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 ? '</tr><tr>' : '<tr>');
|
||
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(
|
||
'<td class="flotr-legend-color-box">',
|
||
'<div style="border:1px solid ', legend.labelBoxBorderColor, ';padding:1px">',
|
||
'<div style="width:', (boxWidth-1), 'px;height:', (boxHeight-1), 'px;border:1px solid ', series[i].color, '">', // Border
|
||
'<div style="width:', boxWidth, 'px;height:', boxHeight, 'px;', color, '"></div>', // Background
|
||
'</div>',
|
||
'</div>',
|
||
'</td>',
|
||
'<td class="flotr-legend-label">', label, '</td>'
|
||
);
|
||
}
|
||
if(rowStarted) fragments.push('</tr>');
|
||
|
||
if(fragments.length > 0){
|
||
var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join('') + '</table>';
|
||
if(legend.container){
|
||
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('<div class="flotr-tabs-group" style="position:absolute;left:0px;width:'+this.canvasWidth+'px"></div>'),
|
||
graph = D.node('<div style="float:left" class="flotr-tab selected">'+this.options.spreadsheet.tabGraphLabel+'</div>'),
|
||
data = D.node('<div style="float:left" class="flotr-tab">'+this.options.spreadsheet.tabDataLabel+'</div>'),
|
||
offset;
|
||
|
||
ss.tabsContainer = container;
|
||
ss.tabs = { graph : graph, data : data };
|
||
|
||
D.insert(container, graph);
|
||
D.insert(container, data);
|
||
D.insert(this.el, container);
|
||
|
||
offset = D.size(data).height + 2;
|
||
this.plotOffset.bottom += offset;
|
||
|
||
D.setStyles(container, {top: this.canvasHeight-offset+'px'});
|
||
|
||
this.
|
||
observe(graph, 'click', function(){ss.showTab('graph');}).
|
||
observe(data, 'click', function(){ss.showTab('data');});
|
||
if (this.options.spreadsheet.initialTab !== 'graph'){
|
||
ss.showTab(this.options.spreadsheet.initialTab);
|
||
}
|
||
}
|
||
},
|
||
/**
|
||
* Builds a matrix of the data to make the correspondance between the x values and the y values :
|
||
* X value => Y values from the axes
|
||
* @return {Array} The data grid
|
||
*/
|
||
loadDataGrid: function(){
|
||
if (this.seriesData) return this.seriesData;
|
||
|
||
var s = this.series,
|
||
rows = {};
|
||
|
||
/* The data grid is a 2 dimensions array. There is a row for each X value.
|
||
* Each row contains the x value and the corresponding y value for each serie ('undefined' if there isn't one)
|
||
**/
|
||
_.each(s, function(serie, i){
|
||
_.each(serie.data, function (v) {
|
||
var x = v[0],
|
||
y = v[1],
|
||
r = rows[x];
|
||
if (r) {
|
||
r[i+1] = y;
|
||
} else {
|
||
var newRow = [];
|
||
newRow[0] = x;
|
||
newRow[i+1] = y;
|
||
rows[x] = newRow;
|
||
}
|
||
});
|
||
});
|
||
|
||
// The data grid is sorted by x value
|
||
this.seriesData = _.sortBy(rows, function(row, x){
|
||
return parseInt(x, 10);
|
||
});
|
||
return this.seriesData;
|
||
},
|
||
/**
|
||
* Constructs the data table for the spreadsheet
|
||
* @todo make a spreadsheet manager (Flotr.Spreadsheet)
|
||
* @return {Element} The resulting table element
|
||
*/
|
||
constructDataGrid: function(){
|
||
// If the data grid has already been built, nothing to do here
|
||
if (this.spreadsheet.datagrid) return this.spreadsheet.datagrid;
|
||
|
||
var s = this.series,
|
||
datagrid = this.spreadsheet.loadDataGrid(),
|
||
colgroup = ['<colgroup><col />'],
|
||
buttonDownload, buttonSelect, t;
|
||
|
||
// First row : series' labels
|
||
var html = ['<table class="flotr-datagrid"><tr class="first-row">'];
|
||
html.push('<th> </th>');
|
||
_.each(s, function(serie,i){
|
||
html.push('<th scope="col">'+(serie.label || String.fromCharCode(65+i))+'</th>');
|
||
colgroup.push('<col />');
|
||
});
|
||
html.push('</tr>');
|
||
// Data rows
|
||
_.each(datagrid, function(row){
|
||
html.push('<tr>');
|
||
_.times(s.length+1, function(i){
|
||
var tag = 'td',
|
||
value = row[i],
|
||
// TODO: do we really want to handle problems with floating point
|
||
// precision here?
|
||
content = (!_.isUndefined(value) ? Math.round(value*100000)/100000 : '');
|
||
if (i === 0) {
|
||
tag = 'th';
|
||
var label = getRowLabel.call(this, content);
|
||
if (label) content = label;
|
||
}
|
||
|
||
html.push('<'+tag+(tag=='th'?' scope="row"':'')+'>'+content+'</'+tag+'>');
|
||
}, this);
|
||
html.push('</tr>');
|
||
}, this);
|
||
colgroup.push('</colgroup>');
|
||
t = D.node(html.join(''));
|
||
|
||
/**
|
||
* @TODO disabled this
|
||
if (!Flotr.isIE || Flotr.isIE == 9) {
|
||
function handleMouseout(){
|
||
t.select('colgroup col.hover, th.hover').invoke('removeClassName', 'hover');
|
||
}
|
||
function handleMouseover(e){
|
||
var td = e.element(),
|
||
siblings = td.previousSiblings();
|
||
t.select('th[scope=col]')[siblings.length-1].addClassName('hover');
|
||
t.select('colgroup col')[siblings.length].addClassName('hover');
|
||
}
|
||
_.each(t.select('td'), function(td) {
|
||
Flotr.EventAdapter.
|
||
observe(td, 'mouseover', handleMouseover).
|
||
observe(td, 'mouseout', handleMouseout);
|
||
});
|
||
}
|
||
*/
|
||
|
||
buttonDownload = D.node(
|
||
'<button type="button" class="flotr-datagrid-toolbar-button">' +
|
||
this.options.spreadsheet.toolbarDownload +
|
||
'</button>');
|
||
|
||
buttonSelect = D.node(
|
||
'<button type="button" class="flotr-datagrid-toolbar-button">' +
|
||
this.options.spreadsheet.toolbarSelectAll+
|
||
'</button>');
|
||
|
||
this.
|
||
observe(buttonDownload, 'click', _.bind(this.spreadsheet.downloadCSV, this)).
|
||
observe(buttonSelect, 'click', _.bind(this.spreadsheet.selectAllData, this));
|
||
|
||
var toolbar = D.node('<div class="flotr-datagrid-toolbar"></div>');
|
||
D.insert(toolbar, buttonDownload);
|
||
D.insert(toolbar, buttonSelect);
|
||
|
||
var containerHeight =this.canvasHeight - D.size(this.spreadsheet.tabsContainer).height-2,
|
||
container = D.node('<div class="flotr-datagrid-container" style="position:absolute;left:0px;top:0px;width:'+
|
||
this.canvasWidth+'px;height:'+containerHeight+'px;overflow:auto;z-index:10"></div>');
|
||
|
||
D.insert(container, toolbar);
|
||
D.insert(container, t);
|
||
D.insert(this.el, container);
|
||
this.spreadsheet.datagrid = t;
|
||
this.spreadsheet.container = container;
|
||
|
||
return t;
|
||
},
|
||
/**
|
||
* Shows the specified tab, by its name
|
||
* @todo make a tab manager (Flotr.Tabs)
|
||
* @param {String} tabName - The tab name
|
||
*/
|
||
showTab: function(tabName){
|
||
if (this.spreadsheet.activeTab === tabName){
|
||
return;
|
||
}
|
||
switch(tabName) {
|
||
case 'graph':
|
||
D.hide(this.spreadsheet.container);
|
||
D.removeClass(this.spreadsheet.tabs.data, 'selected');
|
||
D.addClass(this.spreadsheet.tabs.graph, 'selected');
|
||
break;
|
||
case 'data':
|
||
if (!this.spreadsheet.datagrid)
|
||
this.spreadsheet.constructDataGrid();
|
||
D.show(this.spreadsheet.container);
|
||
D.addClass(this.spreadsheet.tabs.data, 'selected');
|
||
D.removeClass(this.spreadsheet.tabs.graph, 'selected');
|
||
break;
|
||
default:
|
||
throw 'Illegal tab name: ' + tabName;
|
||
}
|
||
this.spreadsheet.activeTab = tabName;
|
||
},
|
||
/**
|
||
* Selects the data table in the DOM for copy/paste
|
||
*/
|
||
selectAllData: function(){
|
||
if (this.spreadsheet.tabs) {
|
||
var selection, range, doc, win, node = this.spreadsheet.constructDataGrid();
|
||
|
||
this.spreadsheet.showTab('data');
|
||
|
||
// deferred to be able to select the table
|
||
setTimeout(function () {
|
||
if ((doc = node.ownerDocument) && (win = doc.defaultView) &&
|
||
win.getSelection && doc.createRange &&
|
||
(selection = window.getSelection()) &&
|
||
selection.removeAllRanges) {
|
||
range = doc.createRange();
|
||
range.selectNode(node);
|
||
selection.removeAllRanges();
|
||
selection.addRange(range);
|
||
}
|
||
else if (document.body && document.body.createTextRange &&
|
||
(range = document.body.createTextRange())) {
|
||
range.moveToElementText(node);
|
||
range.select();
|
||
}
|
||
}, 0);
|
||
return true;
|
||
}
|
||
else return false;
|
||
},
|
||
/**
|
||
* Converts the data into CSV in order to download a file
|
||
*/
|
||
downloadCSV: function(){
|
||
var csv = '',
|
||
series = this.series,
|
||
options = this.options,
|
||
dg = this.spreadsheet.loadDataGrid(),
|
||
separator = encodeURIComponent(options.spreadsheet.csvFileSeparator);
|
||
|
||
if (options.spreadsheet.decimalSeparator === options.spreadsheet.csvFileSeparator) {
|
||
throw "The decimal separator is the same as the column separator ("+options.spreadsheet.decimalSeparator+")";
|
||
}
|
||
|
||
// The first row
|
||
_.each(series, function(serie, i){
|
||
csv += separator+'"'+(serie.label || String.fromCharCode(65+i)).replace(/\"/g, '\\"')+'"';
|
||
});
|
||
|
||
csv += "%0D%0A"; // \r\n
|
||
|
||
// For each row
|
||
csv += _.reduce(dg, function(memo, row){
|
||
var rowLabel = getRowLabel.call(this, row[0]) || '';
|
||
rowLabel = '"'+(rowLabel+'').replace(/\"/g, '\\"')+'"';
|
||
var numbers = row.slice(1).join(separator);
|
||
if (options.spreadsheet.decimalSeparator !== '.') {
|
||
numbers = numbers.replace(/\./g, options.spreadsheet.decimalSeparator);
|
||
}
|
||
return memo + rowLabel+separator+numbers+"%0D%0A"; // \t and \r\n
|
||
}, '', this);
|
||
|
||
if (Flotr.isIE && Flotr.isIE < 9) {
|
||
csv = csv.replace(new RegExp(separator, 'g'), decodeURIComponent(separator)).replace(/%0A/g, '\n').replace(/%0D/g, '\r');
|
||
window.open().document.write(csv);
|
||
}
|
||
else window.open('data:text/csv,'+csv);
|
||
}
|
||
});
|
||
})();
|
||
|
||
(function () {
|
||
|
||
var D = Flotr.DOM;
|
||
|
||
Flotr.addPlugin('titles', {
|
||
callbacks: {
|
||
'flotr:afterdraw': function() {
|
||
this.titles.drawTitles();
|
||
}
|
||
},
|
||
/**
|
||
* Draws the title and the subtitle
|
||
*/
|
||
drawTitles : function () {
|
||
var html,
|
||
options = this.options,
|
||
margin = options.grid.labelMargin,
|
||
ctx = this.ctx,
|
||
a = this.axes;
|
||
|
||
if (!options.HtmlText && this.textEnabled) {
|
||
var style = {
|
||
size: options.fontSize,
|
||
color: options.grid.color,
|
||
textAlign: 'center'
|
||
};
|
||
|
||
// Add subtitle
|
||
if (options.subtitle){
|
||
Flotr.drawText(
|
||
ctx, options.subtitle,
|
||
this.plotOffset.left + this.plotWidth/2,
|
||
this.titleHeight + this.subtitleHeight - 2,
|
||
style
|
||
);
|
||
}
|
||
|
||
style.weight = 1.5;
|
||
style.size *= 1.5;
|
||
|
||
// Add title
|
||
if (options.title){
|
||
Flotr.drawText(
|
||
ctx, options.title,
|
||
this.plotOffset.left + this.plotWidth/2,
|
||
this.titleHeight - 2,
|
||
style
|
||
);
|
||
}
|
||
|
||
style.weight = 1.8;
|
||
style.size *= 0.8;
|
||
|
||
// Add x axis title
|
||
if (a.x.options.title && a.x.used){
|
||
style.textAlign = a.x.options.titleAlign || 'center';
|
||
style.textBaseline = 'top';
|
||
style.angle = Flotr.toRad(a.x.options.titleAngle);
|
||
style = Flotr.getBestTextAlign(style.angle, style);
|
||
Flotr.drawText(
|
||
ctx, a.x.options.title,
|
||
this.plotOffset.left + this.plotWidth/2,
|
||
this.plotOffset.top + a.x.maxLabel.height + this.plotHeight + 2 * margin,
|
||
style
|
||
);
|
||
}
|
||
|
||
// Add x2 axis title
|
||
if (a.x2.options.title && a.x2.used){
|
||
style.textAlign = a.x2.options.titleAlign || 'center';
|
||
style.textBaseline = 'bottom';
|
||
style.angle = Flotr.toRad(a.x2.options.titleAngle);
|
||
style = Flotr.getBestTextAlign(style.angle, style);
|
||
Flotr.drawText(
|
||
ctx, a.x2.options.title,
|
||
this.plotOffset.left + this.plotWidth/2,
|
||
this.plotOffset.top - a.x2.maxLabel.height - 2 * margin,
|
||
style
|
||
);
|
||
}
|
||
|
||
// Add y axis title
|
||
if (a.y.options.title && a.y.used){
|
||
style.textAlign = a.y.options.titleAlign || 'right';
|
||
style.textBaseline = 'middle';
|
||
style.angle = Flotr.toRad(a.y.options.titleAngle);
|
||
style = Flotr.getBestTextAlign(style.angle, style);
|
||
Flotr.drawText(
|
||
ctx, a.y.options.title,
|
||
this.plotOffset.left - a.y.maxLabel.width - 2 * margin,
|
||
this.plotOffset.top + this.plotHeight / 2,
|
||
style
|
||
);
|
||
}
|
||
|
||
// Add y2 axis title
|
||
if (a.y2.options.title && a.y2.used){
|
||
style.textAlign = a.y2.options.titleAlign || 'left';
|
||
style.textBaseline = 'middle';
|
||
style.angle = Flotr.toRad(a.y2.options.titleAngle);
|
||
style = Flotr.getBestTextAlign(style.angle, style);
|
||
Flotr.drawText(
|
||
ctx, a.y2.options.title,
|
||
this.plotOffset.left + this.plotWidth + a.y2.maxLabel.width + 2 * margin,
|
||
this.plotOffset.top + this.plotHeight / 2,
|
||
style
|
||
);
|
||
}
|
||
}
|
||
else {
|
||
html = [];
|
||
|
||
// Add title
|
||
if (options.title)
|
||
html.push(
|
||
'<div style="position:absolute;top:0;left:',
|
||
this.plotOffset.left, 'px;font-size:1em;font-weight:bold;text-align:center;width:',
|
||
this.plotWidth,'px;" class="flotr-title">', options.title, '</div>'
|
||
);
|
||
|
||
// Add subtitle
|
||
if (options.subtitle)
|
||
html.push(
|
||
'<div style="position:absolute;top:', this.titleHeight, 'px;left:',
|
||
this.plotOffset.left, 'px;font-size:smaller;text-align:center;width:',
|
||
this.plotWidth, 'px;" class="flotr-subtitle">', options.subtitle, '</div>'
|
||
);
|
||
|
||
html.push('</div>');
|
||
|
||
html.push('<div class="flotr-axis-title" style="font-weight:bold;">');
|
||
|
||
// Add x axis title
|
||
if (a.x.options.title && a.x.used)
|
||
html.push(
|
||
'<div style="position:absolute;top:',
|
||
(this.plotOffset.top + this.plotHeight + options.grid.labelMargin + a.x.titleSize.height),
|
||
'px;left:', this.plotOffset.left, 'px;width:', this.plotWidth,
|
||
'px;text-align:', a.x.options.titleAlign, ';" class="flotr-axis-title flotr-axis-title-x1">', a.x.options.title, '</div>'
|
||
);
|
||
|
||
// Add x2 axis title
|
||
if (a.x2.options.title && a.x2.used)
|
||
html.push(
|
||
'<div style="position:absolute;top:0;left:', this.plotOffset.left, 'px;width:',
|
||
this.plotWidth, 'px;text-align:', a.x2.options.titleAlign, ';" class="flotr-axis-title flotr-axis-title-x2">', a.x2.options.title, '</div>'
|
||
);
|
||
|
||
// Add y axis title
|
||
if (a.y.options.title && a.y.used)
|
||
html.push(
|
||
'<div style="position:absolute;top:',
|
||
(this.plotOffset.top + this.plotHeight/2 - a.y.titleSize.height/2),
|
||
'px;left:0;text-align:', a.y.options.titleAlign, ';" class="flotr-axis-title flotr-axis-title-y1">', a.y.options.title, '</div>'
|
||
);
|
||
|
||
// Add y2 axis title
|
||
if (a.y2.options.title && a.y2.used)
|
||
html.push(
|
||
'<div style="position:absolute;top:',
|
||
(this.plotOffset.top + this.plotHeight/2 - a.y.titleSize.height/2),
|
||
'px;right:0;text-align:', a.y2.options.titleAlign, ';" class="flotr-axis-title flotr-axis-title-y2">', a.y2.options.title, '</div>'
|
||
);
|
||
|
||
html = html.join('');
|
||
|
||
var div = D.create('div');
|
||
D.setStyles({
|
||
color: options.grid.color
|
||
});
|
||
div.className = 'flotr-titles';
|
||
D.insert(this.el, div);
|
||
D.insert(div, html);
|
||
}
|
||
}
|
||
});
|
||
})();
|