2 * bean.js - copyright Jacob Thornton 2011
3 * https://github.com/fat/bean
6 * dean edwards: http://dean.edwards.name/
7 * dperini: https://github.com/dperini/nwevents
8 * the entire mootools team: github.com/mootools/mootools-core
10 /*global module:true, define:true*/
11 !function (name, context, definition) {
12 if (typeof module !== 'undefined') module.exports = definition(name, context);
13 else if (typeof define === 'function' && typeof define.amd === 'object') define(definition);
14 else context[name] = definition(name, context);
15 }('bean', this, function (name, context) {
18 , overOut = /over|out/
19 , namespaceRegex = /[^\.]*(?=\..*)\.|.*/
21 , addEvent = 'addEventListener'
22 , attachEvent = 'attachEvent'
23 , removeEvent = 'removeEventListener'
24 , detachEvent = 'detachEvent'
25 , doc = document || {}
26 , root = doc.documentElement || {}
27 , W3C_MODEL = root[addEvent]
28 , eventSupport = W3C_MODEL ? addEvent : attachEvent
29 , slice = Array.prototype.slice
30 , mouseTypeRegex = /click|mouse|menu|drag|drop/i
31 , touchTypeRegex = /^touch|^gesture/i
32 , ONE = { one: 1 } // singleton for quick matching making add() do one()
34 , nativeEvents = (function (hash, events, i) {
35 for (i = 0; i < events.length; i++)
39 'click dblclick mouseup mousedown contextmenu ' + // mouse buttons
40 'mousewheel DOMMouseScroll ' + // mouse wheel
41 'mouseover mouseout mousemove selectstart selectend ' + // mouse movement
42 'keydown keypress keyup ' + // keyboard
43 'orientationchange ' + // mobile
44 'focus blur change reset select submit ' + // form elements
45 'load unload beforeunload resize move DOMContentLoaded readystatechange ' + // window
46 'error abort scroll ' + // misc
47 (W3C_MODEL ? // element.fireEvent('onXYZ'... is not forgiving if we try to fire an event
48 // that doesn't actually exist, so make sure we only do these on newer browsers
49 'show ' + // mouse buttons
50 'input invalid ' + // form elements
51 'touchstart touchmove touchend touchcancel ' + // touch
52 'gesturestart gesturechange gestureend ' + // gesture
53 'message readystatechange pageshow pagehide popstate ' + // window
54 'hashchange offline online ' + // window
55 'afterprint beforeprint ' + // printing
56 'dragstart dragenter dragover dragleave drag drop dragend ' + // dnd
57 'loadstart progress suspend emptied stalled loadmetadata ' + // media
58 'loadeddata canplay canplaythrough playing waiting seeking ' + // media
59 'seeked ended durationchange timeupdate play pause ratechange ' + // media
60 'volumechange cuechange ' + // media
61 'checking noupdate downloading cached updateready obsolete ' + // appcache
66 , customEvents = (function () {
67 function isDescendant(parent, node) {
68 while ((node = node.parentNode) !== null) {
69 if (node === parent) return true
74 function check(event) {
75 var related = event.relatedTarget
76 if (!related) return related === null
77 return (related !== this && related.prefix !== 'xul' && !/document/.test(this.toString()) && !isDescendant(this, related))
81 mouseenter: { base: 'mouseover', condition: check }
82 , mouseleave: { base: 'mouseout', condition: check }
83 , mousewheel: { base: /Firefox/.test(navigator.userAgent) ? 'DOMMouseScroll' : 'mousewheel' }
87 , fixEvent = (function () {
88 var commonProps = 'altKey attrChange attrName bubbles cancelable ctrlKey currentTarget detail eventPhase getModifierState isTrusted metaKey relatedNode relatedTarget shiftKey srcElement target timeStamp type view which'.split(' ')
89 , mouseProps = commonProps.concat('button buttons clientX clientY dataTransfer fromElement offsetX offsetY pageX pageY screenX screenY toElement'.split(' '))
90 , keyProps = commonProps.concat('char charCode key keyCode'.split(' '))
91 , touchProps = commonProps.concat('touches targetTouches changedTouches scale rotation'.split(' '))
92 , preventDefault = 'preventDefault'
93 , createPreventDefault = function (event) {
95 if (event[preventDefault])
96 event[preventDefault]()
98 event.returnValue = false
101 , stopPropagation = 'stopPropagation'
102 , createStopPropagation = function (event) {
104 if (event[stopPropagation])
105 event[stopPropagation]()
107 event.cancelBubble = true
110 , createStop = function (synEvent) {
112 synEvent[preventDefault]()
113 synEvent[stopPropagation]()
114 synEvent.stopped = true
117 , copyProps = function (event, result, props) {
119 for (i = props.length; i--;) {
121 if (!(p in result) && p in event) result[p] = event[p]
125 return function (event, isNative) {
126 var result = { originalEvent: event, isNative: isNative }
132 , target = event.target || event.srcElement
134 result[preventDefault] = createPreventDefault(event)
135 result[stopPropagation] = createStopPropagation(event)
136 result.stop = createStop(result)
137 result.target = target && target.nodeType === 3 ? target.parentNode : target
139 if (isNative) { // we only need basic augmentation on custom events, the rest is too expensive
140 if (type.indexOf('key') !== -1) {
142 result.keyCode = event.which || event.keyCode
143 } else if (mouseTypeRegex.test(type)) {
145 result.rightClick = event.which === 3 || event.button === 2
146 result.pos = { x: 0, y: 0 }
147 if (event.pageX || event.pageY) {
148 result.clientX = event.pageX
149 result.clientY = event.pageY
150 } else if (event.clientX || event.clientY) {
151 result.clientX = event.clientX + doc.body.scrollLeft + root.scrollLeft
152 result.clientY = event.clientY + doc.body.scrollTop + root.scrollTop
154 if (overOut.test(type))
155 result.relatedTarget = event.relatedTarget || event[(type === 'mouseover' ? 'from' : 'to') + 'Element']
156 } else if (touchTypeRegex.test(type)) {
159 copyProps(event, result, props || commonProps)
165 // if we're in old IE we can't do onpropertychange on doc or win so we use doc.documentElement for both
166 , targetElement = function (element, isNative) {
167 return !W3C_MODEL && !isNative && (element === doc || element === win) ? root : element
170 // we use one of these per listener, of any type
171 , RegEntry = (function () {
172 function entry(element, type, handler, original, namespaces) {
173 this.element = element
175 this.handler = handler
176 this.original = original
177 this.namespaces = namespaces
178 this.custom = customEvents[type]
179 this.isNative = nativeEvents[type] && element[eventSupport]
180 this.eventType = W3C_MODEL || this.isNative ? type : 'propertychange'
181 this.customType = !W3C_MODEL && !this.isNative && type
182 this.target = targetElement(element, this.isNative)
183 this.eventSupport = this.target[eventSupport]
187 // given a list of namespaces, is our entry in any of them?
188 inNamespaces: function (checkNamespaces) {
190 if (!checkNamespaces)
192 if (!this.namespaces)
194 for (i = checkNamespaces.length; i--;) {
195 for (j = this.namespaces.length; j--;) {
196 if (checkNamespaces[i] === this.namespaces[j])
203 // match by element, original fn (opt), handler fn (opt)
204 , matches: function (checkElement, checkOriginal, checkHandler) {
205 return this.element === checkElement &&
206 (!checkOriginal || this.original === checkOriginal) &&
207 (!checkHandler || this.handler === checkHandler)
214 , registry = (function () {
215 // our map stores arrays by event type, just because it's better than storing
216 // everything in a single array. uses '$' as a prefix for the keys for safety
219 // generic functional search of our registry for matching listeners,
220 // `fn` returns false to break out of the loop
221 , forAll = function (element, type, original, handler, fn) {
222 if (!type || type === '*') {
223 // search the whole registry
225 if (t.charAt(0) === '$')
226 forAll(element, t.substr(1), original, handler, fn)
229 var i = 0, l, list = map['$' + type], all = element === '*'
232 for (l = list.length; i < l; i++) {
233 if (all || list[i].matches(element, original, handler))
234 if (!fn(list[i], list, i, type))
240 , has = function (element, type, original) {
241 // we're not using forAll here simply because it's a bit slower and this
243 var i, list = map['$' + type]
245 for (i = list.length; i--;) {
246 if (list[i].matches(element, original, null))
253 , get = function (element, type, original) {
255 forAll(element, type, original, null, function (entry) { return entries.push(entry) })
259 , put = function (entry) {
260 (map['$' + entry.type] || (map['$' + entry.type] = [])).push(entry)
264 , del = function (entry) {
265 forAll(entry.element, entry.type, null, entry.handler, function (entry, list, i) {
267 if (list.length === 0)
268 delete map['$' + entry.type]
273 // dump all entries, used for onunload
274 , entries = function () {
277 if (t.charAt(0) === '$')
278 entries = entries.concat(map[t])
283 return { has: has, get: get, put: put, del: del, entries: entries }
286 // add and remove listeners to DOM elements
287 , listener = W3C_MODEL ? function (element, type, fn, add) {
288 element[add ? addEvent : removeEvent](type, fn, false)
289 } : function (element, type, fn, add, custom) {
290 if (custom && add && element['_on' + custom] === null)
291 element['_on' + custom] = 0
292 element[add ? attachEvent : detachEvent]('on' + type, fn)
295 , nativeHandler = function (element, fn, args) {
296 return function (event) {
297 event = fixEvent(event || ((this.ownerDocument || this.document || this).parentWindow || win).event, true)
298 return fn.apply(element, [event].concat(args))
302 , customHandler = function (element, fn, type, condition, args, isNative) {
303 return function (event) {
304 if (condition ? condition.apply(this, arguments) : W3C_MODEL ? true : event && event.propertyName === '_on' + type || !event) {
306 event = fixEvent(event || ((this.ownerDocument || this.document || this).parentWindow || win).event, isNative)
307 fn.apply(element, event && (!args || args.length === 0) ? arguments : slice.call(arguments, event ? 0 : 1).concat(args))
312 , once = function (rm, element, type, fn, originalFn) {
313 // wrap the handler in a handler that does a remove as well
315 rm(element, type, originalFn)
316 fn.apply(this, arguments)
320 , removeListener = function (element, orgType, handler, namespaces) {
322 , type = (orgType && orgType.replace(nameRegex, ''))
323 , handlers = registry.get(element, type, handler)
325 for (i = 0, l = handlers.length; i < l; i++) {
326 if (handlers[i].inNamespaces(namespaces)) {
327 if ((entry = handlers[i]).eventSupport)
328 listener(entry.target, entry.eventType, entry.handler, false, entry.type)
329 // TODO: this is problematic, we have a registry.get() and registry.del() that
330 // both do registry searches so we waste cycles doing this. Needs to be rolled into
331 // a single registry.forAll(fn) that removes while finding, but the catch is that
332 // we'll be splicing the arrays that we're iterating over. Needs extra tests to
333 // make sure we don't screw it up. @rvagg
339 , addListener = function (element, orgType, fn, originalFn, args) {
341 , type = orgType.replace(nameRegex, '')
342 , namespaces = orgType.replace(namespaceRegex, '').split('.')
344 if (registry.has(element, type, fn))
345 return element // no dupe
346 if (type === 'unload')
347 fn = once(removeListener, element, type, fn, originalFn) // self clean-up
348 if (customEvents[type]) {
349 if (customEvents[type].condition)
350 fn = customHandler(element, fn, type, customEvents[type].condition, true)
351 type = customEvents[type].base || type
353 entry = registry.put(new RegEntry(element, type, fn, originalFn, namespaces[0] && namespaces))
354 entry.handler = entry.isNative ?
355 nativeHandler(element, entry.handler, args) :
356 customHandler(element, entry.handler, type, false, args, false)
357 if (entry.eventSupport)
358 listener(entry.target, entry.eventType, entry.handler, true, entry.customType)
361 , del = function (selector, fn, $) {
362 return function (e) {
363 var target, i, array = typeof selector === 'string' ? $(selector, this) : selector
364 for (target = e.target; target && target !== this; target = target.parentNode) {
365 for (i = array.length; i--;) {
366 if (array[i] === target) {
367 return fn.apply(target, arguments)
374 , remove = function (element, typeSpec, fn) {
375 var k, m, type, namespaces, i
376 , rm = removeListener
377 , isString = typeSpec && typeof typeSpec === 'string'
379 if (isString && typeSpec.indexOf(' ') > 0) {
380 // remove(el, 't1 t2 t3', fn) or remove(el, 't1 t2 t3')
381 typeSpec = typeSpec.split(' ')
382 for (i = typeSpec.length; i--;)
383 remove(element, typeSpec[i], fn)
386 type = isString && typeSpec.replace(nameRegex, '')
387 if (type && customEvents[type])
388 type = customEvents[type].type
389 if (!typeSpec || isString) {
390 // remove(el) or remove(el, t1.ns) or remove(el, .ns) or remove(el, .ns1.ns2.ns3)
391 if (namespaces = isString && typeSpec.replace(namespaceRegex, ''))
392 namespaces = namespaces.split('.')
393 rm(element, type, fn, namespaces)
394 } else if (typeof typeSpec === 'function') {
396 rm(element, null, typeSpec)
398 // remove(el, { t1: fn1, t2, fn2 })
399 for (k in typeSpec) {
400 if (typeSpec.hasOwnProperty(k))
401 remove(element, k, typeSpec[k])
407 , add = function (element, events, fn, delfn, $) {
408 var type, types, i, args
410 , isDel = fn && typeof fn === 'string'
412 if (events && !fn && typeof events === 'object') {
413 for (type in events) {
414 if (events.hasOwnProperty(type))
415 add.apply(this, [ element, type, events[type] ])
418 args = arguments.length > 3 ? slice.call(arguments, 3) : []
419 types = (isDel ? fn : events).split(' ')
420 isDel && (fn = del(events, (originalFn = delfn), $)) && (args = slice.call(args, 1))
421 // special case for one()
422 this === ONE && (fn = once(remove, element, events, fn, originalFn))
423 for (i = types.length; i--;) addListener(element, types[i], fn, originalFn, args)
428 , one = function () {
429 return add.apply(ONE, arguments)
432 , fireListener = W3C_MODEL ? function (isNative, type, element) {
433 var evt = doc.createEvent(isNative ? 'HTMLEvents' : 'UIEvents')
434 evt[isNative ? 'initEvent' : 'initUIEvent'](type, true, true, win, 1)
435 element.dispatchEvent(evt)
436 } : function (isNative, type, element) {
437 element = targetElement(element, isNative)
438 // if not-native then we're using onpropertychange so we just increment a custom property
439 isNative ? element.fireEvent('on' + type, doc.createEventObject()) : element['_on' + type]++
442 , fire = function (element, type, args) {
443 var i, j, l, names, handlers
444 , types = type.split(' ')
446 for (i = types.length; i--;) {
447 type = types[i].replace(nameRegex, '')
448 if (names = types[i].replace(namespaceRegex, ''))
449 names = names.split('.')
450 if (!names && !args && element[eventSupport]) {
451 fireListener(nativeEvents[type], type, element)
453 // non-native event, either because of a namespace, arguments or a non DOM element
454 // iterate over all listeners and manually 'fire'
455 handlers = registry.get(element, type)
456 args = [false].concat(args)
457 for (j = 0, l = handlers.length; j < l; j++) {
458 if (handlers[j].inNamespaces(names))
459 handlers[j].handler.apply(element, args)
466 , clone = function (element, from, type) {
468 , handlers = registry.get(from, type)
469 , l = handlers.length
472 handlers[i].original && add(element, handlers[i].type, handlers[i].original)
482 , noConflict: function () {
488 if (win[attachEvent]) {
489 // for IE, clean up on unload to avoid leaks
490 var cleanup = function () {
491 var i, entries = registry.entries()
493 if (entries[i].type && entries[i].type !== 'unload')
494 remove(entries[i].element, entries[i].type)
496 win[detachEvent]('onunload', cleanup)
497 win.CollectGarbage && win.CollectGarbage()
499 win[attachEvent]('onunload', cleanup)
504 // Underscore.js 1.1.7
505 // (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.
506 // Underscore is freely distributable under the MIT license.
507 // Portions of Underscore are inspired or borrowed from Prototype,
508 // Oliver Steele's Functional, and John Resig's Micro-Templating.
509 // For all details and documentation:
510 // http://documentcloud.github.com/underscore
517 // Establish the root object, `window` in the browser, or `global` on the server.
520 // Save the previous value of the `_` variable.
521 var previousUnderscore = root._;
523 // Establish the object that gets returned to break out of a loop iteration.
526 // Save bytes in the minified (but not gzipped) version:
527 var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
529 // Create quick reference variables for speed access to core prototypes.
530 var slice = ArrayProto.slice,
531 unshift = ArrayProto.unshift,
532 toString = ObjProto.toString,
533 hasOwnProperty = ObjProto.hasOwnProperty;
535 // All **ECMAScript 5** native function implementations that we hope to use
536 // are declared here.
538 nativeForEach = ArrayProto.forEach,
539 nativeMap = ArrayProto.map,
540 nativeReduce = ArrayProto.reduce,
541 nativeReduceRight = ArrayProto.reduceRight,
542 nativeFilter = ArrayProto.filter,
543 nativeEvery = ArrayProto.every,
544 nativeSome = ArrayProto.some,
545 nativeIndexOf = ArrayProto.indexOf,
546 nativeLastIndexOf = ArrayProto.lastIndexOf,
547 nativeIsArray = Array.isArray,
548 nativeKeys = Object.keys,
549 nativeBind = FuncProto.bind;
551 // Create a safe reference to the Underscore object for use below.
552 var _ = function(obj) { return new wrapper(obj); };
554 // Export the Underscore object for **CommonJS**, with backwards-compatibility
555 // for the old `require()` API. If we're not in CommonJS, add `_` to the
557 if (typeof module !== 'undefined' && module.exports) {
561 // Exported as a string, for Closure Compiler "advanced" mode.
568 // Collection Functions
569 // --------------------
571 // The cornerstone, an `each` implementation, aka `forEach`.
572 // Handles objects with the built-in `forEach`, arrays, and raw objects.
573 // Delegates to **ECMAScript 5**'s native `forEach` if available.
574 var each = _.each = _.forEach = function(obj, iterator, context) {
575 if (obj == null) return;
576 if (nativeForEach && obj.forEach === nativeForEach) {
577 obj.forEach(iterator, context);
578 } else if (obj.length === +obj.length) {
579 for (var i = 0, l = obj.length; i < l; i++) {
580 if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return;
583 for (var key in obj) {
584 if (hasOwnProperty.call(obj, key)) {
585 if (iterator.call(context, obj[key], key, obj) === breaker) return;
591 // Return the results of applying the iterator to each element.
592 // Delegates to **ECMAScript 5**'s native `map` if available.
593 _.map = function(obj, iterator, context) {
595 if (obj == null) return results;
596 if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
597 each(obj, function(value, index, list) {
598 results[results.length] = iterator.call(context, value, index, list);
603 // **Reduce** builds up a single result from a list of values, aka `inject`,
604 // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
605 _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
606 var initial = memo !== void 0;
607 if (obj == null) obj = [];
608 if (nativeReduce && obj.reduce === nativeReduce) {
609 if (context) iterator = _.bind(iterator, context);
610 return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
612 each(obj, function(value, index, list) {
617 memo = iterator.call(context, memo, value, index, list);
620 if (!initial) throw new TypeError("Reduce of empty array with no initial value");
624 // The right-associative version of reduce, also known as `foldr`.
625 // Delegates to **ECMAScript 5**'s native `reduceRight` if available.
626 _.reduceRight = _.foldr = function(obj, iterator, memo, context) {
627 if (obj == null) obj = [];
628 if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
629 if (context) iterator = _.bind(iterator, context);
630 return memo !== void 0 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
632 var reversed = (_.isArray(obj) ? obj.slice() : _.toArray(obj)).reverse();
633 return _.reduce(reversed, iterator, memo, context);
636 // Return the first value which passes a truth test. Aliased as `detect`.
637 _.find = _.detect = function(obj, iterator, context) {
639 any(obj, function(value, index, list) {
640 if (iterator.call(context, value, index, list)) {
648 // Return all the elements that pass a truth test.
649 // Delegates to **ECMAScript 5**'s native `filter` if available.
650 // Aliased as `select`.
651 _.filter = _.select = function(obj, iterator, context) {
653 if (obj == null) return results;
654 if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
655 each(obj, function(value, index, list) {
656 if (iterator.call(context, value, index, list)) results[results.length] = value;
661 // Return all the elements for which a truth test fails.
662 _.reject = function(obj, iterator, context) {
664 if (obj == null) return results;
665 each(obj, function(value, index, list) {
666 if (!iterator.call(context, value, index, list)) results[results.length] = value;
671 // Determine whether all of the elements match a truth test.
672 // Delegates to **ECMAScript 5**'s native `every` if available.
674 _.every = _.all = function(obj, iterator, context) {
676 if (obj == null) return result;
677 if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
678 each(obj, function(value, index, list) {
679 if (!(result = result && iterator.call(context, value, index, list))) return breaker;
684 // Determine if at least one element in the object matches a truth test.
685 // Delegates to **ECMAScript 5**'s native `some` if available.
687 var any = _.some = _.any = function(obj, iterator, context) {
688 iterator = iterator || _.identity;
690 if (obj == null) return result;
691 if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
692 each(obj, function(value, index, list) {
693 if (result |= iterator.call(context, value, index, list)) return breaker;
698 // Determine if a given value is included in the array or object using `===`.
699 // Aliased as `contains`.
700 _.include = _.contains = function(obj, target) {
702 if (obj == null) return found;
703 if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
704 any(obj, function(value) {
705 if (found = value === target) return true;
710 // Invoke a method (with arguments) on every item in a collection.
711 _.invoke = function(obj, method) {
712 var args = slice.call(arguments, 2);
713 return _.map(obj, function(value) {
714 return (method.call ? method || value : value[method]).apply(value, args);
718 // Convenience version of a common use case of `map`: fetching a property.
719 _.pluck = function(obj, key) {
720 return _.map(obj, function(value){ return value[key]; });
723 // Return the maximum element or (element-based computation).
724 _.max = function(obj, iterator, context) {
725 if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj);
726 var result = {computed : -Infinity};
727 each(obj, function(value, index, list) {
728 var computed = iterator ? iterator.call(context, value, index, list) : value;
729 computed >= result.computed && (result = {value : value, computed : computed});
734 // Return the minimum element (or element-based computation).
735 _.min = function(obj, iterator, context) {
736 if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj);
737 var result = {computed : Infinity};
738 each(obj, function(value, index, list) {
739 var computed = iterator ? iterator.call(context, value, index, list) : value;
740 computed < result.computed && (result = {value : value, computed : computed});
745 // Sort the object's values by a criterion produced by an iterator.
746 _.sortBy = function(obj, iterator, context) {
747 return _.pluck(_.map(obj, function(value, index, list) {
750 criteria : iterator.call(context, value, index, list)
752 }).sort(function(left, right) {
753 var a = left.criteria, b = right.criteria;
754 return a < b ? -1 : a > b ? 1 : 0;
758 // Groups the object's values by a criterion produced by an iterator
759 _.groupBy = function(obj, iterator) {
761 each(obj, function(value, index) {
762 var key = iterator(value, index);
763 (result[key] || (result[key] = [])).push(value);
768 // Use a comparator function to figure out at what index an object should
769 // be inserted so as to maintain order. Uses binary search.
770 _.sortedIndex = function(array, obj, iterator) {
771 iterator || (iterator = _.identity);
772 var low = 0, high = array.length;
774 var mid = (low + high) >> 1;
775 iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid;
780 // Safely convert anything iterable into a real, live array.
781 _.toArray = function(iterable) {
782 if (!iterable) return [];
783 if (iterable.toArray) return iterable.toArray();
784 if (_.isArray(iterable)) return slice.call(iterable);
785 if (_.isArguments(iterable)) return slice.call(iterable);
786 return _.values(iterable);
789 // Return the number of elements in an object.
790 _.size = function(obj) {
791 return _.toArray(obj).length;
797 // Get the first element of an array. Passing **n** will return the first N
798 // values in the array. Aliased as `head`. The **guard** check allows it to work
800 _.first = _.head = function(array, n, guard) {
801 return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
804 // Returns everything but the first entry of the array. Aliased as `tail`.
805 // Especially useful on the arguments object. Passing an **index** will return
806 // the rest of the values in the array from that index onward. The **guard**
807 // check allows it to work with `_.map`.
808 _.rest = _.tail = function(array, index, guard) {
809 return slice.call(array, (index == null) || guard ? 1 : index);
812 // Get the last element of an array.
813 _.last = function(array) {
814 return array[array.length - 1];
817 // Trim out all falsy values from an array.
818 _.compact = function(array) {
819 return _.filter(array, function(value){ return !!value; });
822 // Return a completely flattened version of an array.
823 _.flatten = function(array) {
824 return _.reduce(array, function(memo, value) {
825 if (_.isArray(value)) return memo.concat(_.flatten(value));
826 memo[memo.length] = value;
831 // Return a version of the array that does not contain the specified value(s).
832 _.without = function(array) {
833 return _.difference(array, slice.call(arguments, 1));
836 // Produce a duplicate-free version of the array. If the array has already
837 // been sorted, you have the option of using a faster algorithm.
838 // Aliased as `unique`.
839 _.uniq = _.unique = function(array, isSorted) {
840 return _.reduce(array, function(memo, el, i) {
841 if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) memo[memo.length] = el;
846 // Produce an array that contains the union: each distinct element from all of
847 // the passed-in arrays.
848 _.union = function() {
849 return _.uniq(_.flatten(arguments));
852 // Produce an array that contains every item shared between all the
853 // passed-in arrays. (Aliased as "intersect" for back-compat.)
854 _.intersection = _.intersect = function(array) {
855 var rest = slice.call(arguments, 1);
856 return _.filter(_.uniq(array), function(item) {
857 return _.every(rest, function(other) {
858 return _.indexOf(other, item) >= 0;
863 // Take the difference between one array and another.
864 // Only the elements present in just the first array will remain.
865 _.difference = function(array, other) {
866 return _.filter(array, function(value){ return !_.include(other, value); });
869 // Zip together multiple lists into a single array -- elements that share
870 // an index go together.
872 var args = slice.call(arguments);
873 var length = _.max(_.pluck(args, 'length'));
874 var results = new Array(length);
875 for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i);
879 // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
880 // we need this function. Return the position of the first occurrence of an
881 // item in an array, or -1 if the item is not included in the array.
882 // Delegates to **ECMAScript 5**'s native `indexOf` if available.
883 // If the array is large and already in sort order, pass `true`
884 // for **isSorted** to use binary search.
885 _.indexOf = function(array, item, isSorted) {
886 if (array == null) return -1;
889 i = _.sortedIndex(array, item);
890 return array[i] === item ? i : -1;
892 if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
893 for (i = 0, l = array.length; i < l; i++) if (array[i] === item) return i;
898 // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
899 _.lastIndexOf = function(array, item) {
900 if (array == null) return -1;
901 if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item);
902 var i = array.length;
903 while (i--) if (array[i] === item) return i;
907 // Generate an integer Array containing an arithmetic progression. A port of
908 // the native Python `range()` function. See
909 // [the Python documentation](http://docs.python.org/library/functions.html#range).
910 _.range = function(start, stop, step) {
911 if (arguments.length <= 1) {
915 step = arguments[2] || 1;
917 var len = Math.max(Math.ceil((stop - start) / step), 0);
919 var range = new Array(len);
922 range[idx++] = start;
929 // Function (ahem) Functions
930 // ------------------
932 // Create a function bound to a given object (assigning `this`, and arguments,
933 // optionally). Binding with arguments is also known as `curry`.
934 // Delegates to **ECMAScript 5**'s native `Function.bind` if available.
935 // We check for `func.bind` first, to fail fast when `func` is undefined.
936 _.bind = function(func, obj) {
937 if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
938 var args = slice.call(arguments, 2);
940 return func.apply(obj, args.concat(slice.call(arguments)));
944 // Bind all of an object's methods to that object. Useful for ensuring that
945 // all callbacks defined on an object belong to it.
946 _.bindAll = function(obj) {
947 var funcs = slice.call(arguments, 1);
948 if (funcs.length == 0) funcs = _.functions(obj);
949 each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
953 // Memoize an expensive function by storing its results.
954 _.memoize = function(func, hasher) {
956 hasher || (hasher = _.identity);
958 var key = hasher.apply(this, arguments);
959 return hasOwnProperty.call(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
963 // Delays a function for the given number of milliseconds, and then calls
964 // it with the arguments supplied.
965 _.delay = function(func, wait) {
966 var args = slice.call(arguments, 2);
967 return setTimeout(function(){ return func.apply(func, args); }, wait);
970 // Defers a function, scheduling it to run after the current call stack has
972 _.defer = function(func) {
973 return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
976 // Internal function used to implement `_.throttle` and `_.debounce`.
977 var limit = function(func, wait, debounce) {
980 var context = this, args = arguments;
981 var throttler = function() {
983 func.apply(context, args);
985 if (debounce) clearTimeout(timeout);
986 if (debounce || !timeout) timeout = setTimeout(throttler, wait);
990 // Returns a function, that, when invoked, will only be triggered at most once
991 // during a given window of time.
992 _.throttle = function(func, wait) {
993 return limit(func, wait, false);
996 // Returns a function, that, as long as it continues to be invoked, will not
997 // be triggered. The function will be called after it stops being called for
999 _.debounce = function(func, wait) {
1000 return limit(func, wait, true);
1003 // Returns a function that will be executed at most one time, no matter how
1004 // often you call it. Useful for lazy initialization.
1005 _.once = function(func) {
1006 var ran = false, memo;
1008 if (ran) return memo;
1010 return memo = func.apply(this, arguments);
1014 // Returns the first function passed as an argument to the second,
1015 // allowing you to adjust arguments, run code before and after, and
1016 // conditionally execute the original function.
1017 _.wrap = function(func, wrapper) {
1019 var args = [func].concat(slice.call(arguments));
1020 return wrapper.apply(this, args);
1024 // Returns a function that is the composition of a list of functions, each
1025 // consuming the return value of the function that follows.
1026 _.compose = function() {
1027 var funcs = slice.call(arguments);
1029 var args = slice.call(arguments);
1030 for (var i = funcs.length - 1; i >= 0; i--) {
1031 args = [funcs[i].apply(this, args)];
1037 // Returns a function that will only be executed after being called N times.
1038 _.after = function(times, func) {
1040 if (--times < 1) { return func.apply(this, arguments); }
1048 // Retrieve the names of an object's properties.
1049 // Delegates to **ECMAScript 5**'s native `Object.keys`
1050 _.keys = nativeKeys || function(obj) {
1051 if (obj !== Object(obj)) throw new TypeError('Invalid object');
1053 for (var key in obj) if (hasOwnProperty.call(obj, key)) keys[keys.length] = key;
1057 // Retrieve the values of an object's properties.
1058 _.values = function(obj) {
1059 return _.map(obj, _.identity);
1062 // Return a sorted list of the function names available on the object.
1063 // Aliased as `methods`
1064 _.functions = _.methods = function(obj) {
1066 for (var key in obj) {
1067 if (_.isFunction(obj[key])) names.push(key);
1069 return names.sort();
1072 // Extend a given object with all the properties in passed-in object(s).
1073 _.extend = function(obj) {
1074 each(slice.call(arguments, 1), function(source) {
1075 for (var prop in source) {
1076 if (source[prop] !== void 0) obj[prop] = source[prop];
1082 // Fill in a given object with default properties.
1083 _.defaults = function(obj) {
1084 each(slice.call(arguments, 1), function(source) {
1085 for (var prop in source) {
1086 if (obj[prop] == null) obj[prop] = source[prop];
1092 // Create a (shallow-cloned) duplicate of an object.
1093 _.clone = function(obj) {
1094 return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
1097 // Invokes interceptor with the obj, and then returns obj.
1098 // The primary purpose of this method is to "tap into" a method chain, in
1099 // order to perform operations on intermediate results within the chain.
1100 _.tap = function(obj, interceptor) {
1105 // Perform a deep comparison to check if two objects are equal.
1106 _.isEqual = function(a, b) {
1107 // Check object identity.
1108 if (a === b) return true;
1110 var atype = typeof(a), btype = typeof(b);
1111 if (atype != btype) return false;
1112 // Basic equality test (watch out for coercions).
1113 if (a == b) return true;
1114 // One is falsy and the other truthy.
1115 if ((!a && b) || (a && !b)) return false;
1116 // Unwrap any wrapped objects.
1117 if (a._chain) a = a._wrapped;
1118 if (b._chain) b = b._wrapped;
1119 // One of them implements an isEqual()?
1120 if (a.isEqual) return a.isEqual(b);
1121 if (b.isEqual) return b.isEqual(a);
1122 // Check dates' integer values.
1123 if (_.isDate(a) && _.isDate(b)) return a.getTime() === b.getTime();
1125 if (_.isNaN(a) && _.isNaN(b)) return false;
1126 // Compare regular expressions.
1127 if (_.isRegExp(a) && _.isRegExp(b))
1128 return a.source === b.source &&
1129 a.global === b.global &&
1130 a.ignoreCase === b.ignoreCase &&
1131 a.multiline === b.multiline;
1132 // If a is not an object by this point, we can't handle it.
1133 if (atype !== 'object') return false;
1134 // Check for different array lengths before comparing contents.
1135 if (a.length && (a.length !== b.length)) return false;
1136 // Nothing else worked, deep compare the contents.
1137 var aKeys = _.keys(a), bKeys = _.keys(b);
1138 // Different object sizes?
1139 if (aKeys.length != bKeys.length) return false;
1140 // Recursive comparison of contents.
1141 for (var key in a) if (!(key in b) || !_.isEqual(a[key], b[key])) return false;
1145 // Is a given array or object empty?
1146 _.isEmpty = function(obj) {
1147 if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
1148 for (var key in obj) if (hasOwnProperty.call(obj, key)) return false;
1152 // Is a given value a DOM element?
1153 _.isElement = function(obj) {
1154 return !!(obj && obj.nodeType == 1);
1157 // Is a given value an array?
1158 // Delegates to ECMA5's native Array.isArray
1159 _.isArray = nativeIsArray || function(obj) {
1160 return toString.call(obj) === '[object Array]';
1163 // Is a given variable an object?
1164 _.isObject = function(obj) {
1165 return obj === Object(obj);
1168 // Is a given variable an arguments object?
1169 _.isArguments = function(obj) {
1170 return !!(obj && hasOwnProperty.call(obj, 'callee'));
1173 // Is a given value a function?
1174 _.isFunction = function(obj) {
1175 return !!(obj && obj.constructor && obj.call && obj.apply);
1178 // Is a given value a string?
1179 _.isString = function(obj) {
1180 return !!(obj === '' || (obj && obj.charCodeAt && obj.substr));
1183 // Is a given value a number?
1184 _.isNumber = function(obj) {
1185 return !!(obj === 0 || (obj && obj.toExponential && obj.toFixed));
1188 // Is the given value `NaN`? `NaN` happens to be the only value in JavaScript
1189 // that does not equal itself.
1190 _.isNaN = function(obj) {
1194 // Is a given value a boolean?
1195 _.isBoolean = function(obj) {
1196 return obj === true || obj === false;
1199 // Is a given value a date?
1200 _.isDate = function(obj) {
1201 return !!(obj && obj.getTimezoneOffset && obj.setUTCFullYear);
1204 // Is the given value a regular expression?
1205 _.isRegExp = function(obj) {
1206 return !!(obj && obj.test && obj.exec && (obj.ignoreCase || obj.ignoreCase === false));
1209 // Is a given value equal to null?
1210 _.isNull = function(obj) {
1211 return obj === null;
1214 // Is a given variable undefined?
1215 _.isUndefined = function(obj) {
1216 return obj === void 0;
1219 // Utility Functions
1220 // -----------------
1222 // Run Underscore.js in *noConflict* mode, returning the `_` variable to its
1223 // previous owner. Returns a reference to the Underscore object.
1224 _.noConflict = function() {
1225 root._ = previousUnderscore;
1229 // Keep the identity function around for default iterators.
1230 _.identity = function(value) {
1234 // Run a function **n** times.
1235 _.times = function (n, iterator, context) {
1236 for (var i = 0; i < n; i++) iterator.call(context, i);
1239 // Add your own custom functions to the Underscore object, ensuring that
1240 // they're correctly added to the OOP wrapper as well.
1241 _.mixin = function(obj) {
1242 each(_.functions(obj), function(name){
1243 addToWrapper(name, _[name] = obj[name]);
1247 // Generate a unique integer id (unique within the entire client session).
1248 // Useful for temporary DOM ids.
1250 _.uniqueId = function(prefix) {
1251 var id = idCounter++;
1252 return prefix ? prefix + id : id;
1255 // By default, Underscore uses ERB-style template delimiters, change the
1256 // following template settings to use alternative delimiters.
1257 _.templateSettings = {
1258 evaluate : /<%([\s\S]+?)%>/g,
1259 interpolate : /<%=([\s\S]+?)%>/g
1262 // JavaScript micro-templating, similar to John Resig's implementation.
1263 // Underscore templating handles arbitrary delimiters, preserves whitespace,
1264 // and correctly escapes quotes within interpolated code.
1265 _.template = function(str, data) {
1266 var c = _.templateSettings;
1267 var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' +
1268 'with(obj||{}){__p.push(\'' +
1269 str.replace(/\\/g, '\\\\')
1270 .replace(/'/g, "\\'")
1271 .replace(c.interpolate, function(match, code) {
1272 return "'," + code.replace(/\\'/g, "'") + ",'";
1274 .replace(c.evaluate || null, function(match, code) {
1275 return "');" + code.replace(/\\'/g, "'")
1276 .replace(/[\r\n\t]/g, ' ') + "__p.push('";
1278 .replace(/\r/g, '\\r')
1279 .replace(/\n/g, '\\n')
1280 .replace(/\t/g, '\\t')
1281 + "');}return __p.join('');";
1282 var func = new Function('obj', tmpl);
1283 return data ? func(data) : func;
1289 // If Underscore is called as a function, it returns a wrapped object that
1290 // can be used OO-style. This wrapper holds altered versions of all the
1291 // underscore functions. Wrapped objects may be chained.
1292 var wrapper = function(obj) { this._wrapped = obj; };
1294 // Expose `wrapper.prototype` as `_.prototype`
1295 _.prototype = wrapper.prototype;
1297 // Helper function to continue chaining intermediate results.
1298 var result = function(obj, chain) {
1299 return chain ? _(obj).chain() : obj;
1302 // A method to easily add functions to the OOP wrapper.
1303 var addToWrapper = function(name, func) {
1304 wrapper.prototype[name] = function() {
1305 var args = slice.call(arguments);
1306 unshift.call(args, this._wrapped);
1307 return result(func.apply(_, args), this._chain);
1311 // Add all of the Underscore functions to the wrapper object.
1314 // Add all mutator Array functions to the wrapper.
1315 each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
1316 var method = ArrayProto[name];
1317 wrapper.prototype[name] = function() {
1318 method.apply(this._wrapped, arguments);
1319 return result(this._wrapped, this._chain);
1323 // Add all accessor Array functions to the wrapper.
1324 each(['concat', 'join', 'slice'], function(name) {
1325 var method = ArrayProto[name];
1326 wrapper.prototype[name] = function() {
1327 return result(method.apply(this._wrapped, arguments), this._chain);
1331 // Start chaining a wrapped Underscore object.
1332 wrapper.prototype.chain = function() {
1337 // Extracts the result from a wrapped and chained object.
1338 wrapper.prototype.value = function() {
1339 return this._wrapped;
1344 * Flotr2 (c) 2012 Carl Sutherland
1346 * Special thanks to:
1347 * Flotr: http://code.google.com/p/flotr/ (fork)
1348 * Flot: https://github.com/flot/flot (original fork)
1354 previousFlotr = this.Flotr,
1360 isIphone: /iphone/i.test(navigator.userAgent),
1361 isIE: (navigator.appVersion.indexOf("MSIE") != -1 ? parseFloat(navigator.appVersion.split("MSIE")[1]) : false),
1364 * An object of the registered graph types. Use Flotr.addType(type, object)
1365 * to add your own type.
1370 * The list of the registered plugins
1375 * Can be used to add your own chart type.
1376 * @param {String} name - Type of chart, like 'pies', 'bars' etc.
1377 * @param {String} graphType - The object containing the basic drawing functions (draw, etc)
1379 addType: function(name, graphType){
1380 Flotr.graphTypes[name] = graphType;
1381 Flotr.defaultOptions[name] = graphType.options || {};
1382 Flotr.defaultOptions.defaultType = Flotr.defaultOptions.defaultType || name;
1386 * Can be used to add a plugin
1387 * @param {String} name - The name of the plugin
1388 * @param {String} plugin - The object containing the plugin's data (callbacks, options, function1, function2, ...)
1390 addPlugin: function(name, plugin){
1391 Flotr.plugins[name] = plugin;
1392 Flotr.defaultOptions[name] = plugin.options || {};
1396 * Draws the graph. This function is here for backwards compatibility with Flotr version 0.1.0alpha.
1397 * You could also draw graphs by directly calling Flotr.Graph(element, data, options).
1398 * @param {Element} el - element to insert the graph into
1399 * @param {Object} data - an array or object of dataseries
1400 * @param {Object} options - an object containing options
1401 * @param {Class} _GraphKlass_ - (optional) Class to pass the arguments to, defaults to Flotr.Graph
1402 * @return {Object} returns a new graph object and of course draws the graph.
1404 draw: function(el, data, options, GraphKlass){
1405 GraphKlass = GraphKlass || Flotr.Graph;
1406 return new GraphKlass(el, data, options);
1410 * Recursively merges two objects.
1411 * @param {Object} src - source object (likely the object with the least properties)
1412 * @param {Object} dest - destination object (optional, object with the most properties)
1413 * @return {Object} recursively merged Object
1414 * @TODO See if we can't remove this.
1416 merge: function(src, dest){
1417 var i, v, result = dest || {};
1421 if (v && typeof(v) === 'object') {
1422 if (v.constructor === Array) {
1423 result[i] = this._.clone(v);
1425 v.constructor !== RegExp &&
1426 !this._.isElement(v) &&
1429 result[i] = Flotr.merge(v, (dest ? dest[i] : undefined));
1442 * Recursively clones an object.
1443 * @param {Object} object - The object to clone
1444 * @return {Object} the clone
1445 * @TODO See if we can't remove this.
1447 clone: function(object){
1448 return Flotr.merge(object, {});
1452 * Function calculates the ticksize and returns it.
1453 * @param {Integer} noTicks - number of ticks
1454 * @param {Integer} min - lower bound integer value for the current axis
1455 * @param {Integer} max - upper bound integer value for the current axis
1456 * @param {Integer} decimals - number of decimals for the ticks
1457 * @return {Integer} returns the ticksize in pixels
1459 getTickSize: function(noTicks, min, max, decimals){
1460 var delta = (max - min) / noTicks,
1461 magn = Flotr.getMagnitude(delta),
1463 norm = delta / magn; // Norm is between 1.0 and 10.0.
1465 if(norm < 1.5) tickSize = 1;
1466 else if(norm < 2.25) tickSize = 2;
1467 else if(norm < 3) tickSize = ((decimals === 0) ? 2 : 2.5);
1468 else if(norm < 7.5) tickSize = 5;
1470 return tickSize * magn;
1474 * Default tick formatter.
1475 * @param {String, Integer} val - tick value integer
1476 * @param {Object} axisOpts - the axis' options
1477 * @return {String} formatted tick string
1479 defaultTickFormatter: function(val, axisOpts){
1484 * Formats the mouse tracker values.
1485 * @param {Object} obj - Track value Object {x:..,y:..}
1486 * @return {String} Formatted track string
1488 defaultTrackFormatter: function(obj){
1489 return '('+obj.x+', '+obj.y+')';
1493 * Utility function to convert file size values in bytes to kB, MB, ...
1494 * @param value {Number} - The value to convert
1495 * @param precision {Number} - The number of digits after the comma (default: 2)
1496 * @param base {Number} - The base (default: 1000)
1498 engineeringNotation: function(value, precision, base){
1499 var sizes = ['Y','Z','E','P','T','G','M','k',''],
1500 fractionSizes = ['y','z','a','f','p','n','µ','m',''],
1501 total = sizes.length;
1503 base = base || 1000;
1504 precision = Math.pow(10, precision || 2);
1506 if (value === 0) return 0;
1509 while (total-- && (value >= base)) value /= base;
1512 sizes = fractionSizes;
1513 total = sizes.length;
1514 while (total-- && (value < 1)) value *= base;
1517 return (Math.round(value * precision) / precision) + sizes[total];
1521 * Returns the magnitude of the input value.
1522 * @param {Integer, Float} x - integer or float value
1523 * @return {Integer, Float} returns the magnitude of the input value
1525 getMagnitude: function(x){
1526 return Math.pow(10, Math.floor(Math.log(x) / Math.LN10));
1528 toPixel: function(val){
1529 return Math.floor(val)+0.5;//((val-Math.round(val) < 0.4) ? (Math.floor(val)-0.5) : val);
1531 toRad: function(angle){
1532 return -angle * (Math.PI/180);
1534 floorInBase: function(n, base) {
1535 return base * Math.floor(n / base);
1537 drawText: function(ctx, text, x, y, style) {
1538 if (!ctx.fillText) {
1539 ctx.drawText(text, x, y, style);
1543 style = this._.extend({
1544 size: Flotr.defaultOptions.fontSize,
1547 textBaseline: 'bottom',
1553 ctx.translate(x, y);
1554 ctx.rotate(style.angle);
1555 ctx.fillStyle = style.color;
1556 ctx.font = (style.weight > 1 ? "bold " : "") + (style.size*1.3) + "px sans-serif";
1557 ctx.textAlign = style.textAlign;
1558 ctx.textBaseline = style.textBaseline;
1559 ctx.fillText(text, 0, 0);
1562 getBestTextAlign: function(angle, style) {
1563 style = style || {textAlign: 'center', textBaseline: 'middle'};
1564 angle += Flotr.getTextAngleFromAlign(style);
1566 if (Math.abs(Math.cos(angle)) > 10e-3)
1567 style.textAlign = (Math.cos(angle) > 0 ? 'right' : 'left');
1569 if (Math.abs(Math.sin(angle)) > 10e-3)
1570 style.textBaseline = (Math.sin(angle) > 0 ? 'top' : 'bottom');
1576 'right top' : Math.PI/4,
1577 'center top' : Math.PI/2,
1578 'left top' : 3*(Math.PI/4),
1579 'left middle' : Math.PI,
1580 'left bottom' : -3*(Math.PI/4),
1581 'center bottom': -Math.PI/2,
1582 'right bottom' : -Math.PI/4,
1585 getTextAngleFromAlign: function(style) {
1586 return Flotr.alignTable[style.textAlign+' '+style.textBaseline] || 0;
1588 noConflict : function () {
1589 global.Flotr = previousFlotr;
1594 global.Flotr = Flotr;
1601 Flotr.defaultOptions = {
1602 colors: ['#00A8F0', '#C0D800', '#CB4B4B', '#4DA74D', '#9440ED'], //=> The default colorscheme. When there are > 5 series, additional colors are generated.
1603 ieBackgroundColor: '#FFFFFF', // Background color for excanvas clipping
1604 title: null, // => The graph's title
1605 subtitle: null, // => The graph's subtitle
1606 shadowSize: 4, // => size of the 'fake' shadow
1607 defaultType: null, // => default series type
1608 HtmlText: true, // => wether to draw the text using HTML or on the canvas
1609 fontColor: '#545454', // => default font color
1610 fontSize: 7.5, // => canvas' text font size
1611 resolution: 1, // => resolution of the graph, to have printer-friendly graphs !
1612 parseFloat: true, // => whether to preprocess data for floats (ie. if input is string)
1613 preventDefault: true, // => preventDefault by default for mobile events. Turn off to enable scroll.
1615 ticks: null, // => format: either [1, 3] or [[1, 'a'], 3]
1616 minorTicks: null, // => format: either [1, 3] or [[1, 'a'], 3]
1617 showLabels: true, // => setting to true will show the axis ticks labels, hide otherwise
1618 showMinorLabels: false,// => true to show the axis minor ticks labels, false to hide
1619 labelsAngle: 0, // => labels' angle, in degrees
1620 title: null, // => axis title
1621 titleAngle: 0, // => axis title's angle, in degrees
1622 noTicks: 5, // => number of ticks for automagically generated ticks
1623 minorTickFreq: null, // => number of minor ticks between major ticks for autogenerated ticks
1624 tickFormatter: Flotr.defaultTickFormatter, // => fn: number, Object -> string
1625 tickDecimals: null, // => no. of decimals, null means auto
1626 min: null, // => min. value to show, null means set automatically
1627 max: null, // => max. value to show, null means set automatically
1628 autoscale: false, // => Turns autoscaling on with true
1629 autoscaleMargin: 0, // => margin in % to add if auto-setting min/max
1630 color: null, // => color of the ticks
1631 mode: 'normal', // => can be 'time' or 'normal'
1633 timeMode:'UTC', // => For UTC time ('local' for local time).
1634 timeUnit:'millisecond',// => Unit for time (millisecond, second, minute, hour, day, month, year)
1635 scaling: 'linear', // => Scaling, can be 'linear' or 'logarithmic'
1637 titleAlign: 'center',
1638 margin: true // => Turn off margins with false
1642 ticks: null, // => format: either [1, 3] or [[1, 'a'], 3]
1643 minorTicks: null, // => format: either [1, 3] or [[1, 'a'], 3]
1644 showLabels: true, // => setting to true will show the axis ticks labels, hide otherwise
1645 showMinorLabels: false,// => true to show the axis minor ticks labels, false to hide
1646 labelsAngle: 0, // => labels' angle, in degrees
1647 title: null, // => axis title
1648 titleAngle: 90, // => axis title's angle, in degrees
1649 noTicks: 5, // => number of ticks for automagically generated ticks
1650 minorTickFreq: null, // => number of minor ticks between major ticks for autogenerated ticks
1651 tickFormatter: Flotr.defaultTickFormatter, // => fn: number, Object -> string
1652 tickDecimals: null, // => no. of decimals, null means auto
1653 min: null, // => min. value to show, null means set automatically
1654 max: null, // => max. value to show, null means set automatically
1655 autoscale: false, // => Turns autoscaling on with true
1656 autoscaleMargin: 0, // => margin in % to add if auto-setting min/max
1657 color: null, // => The color of the ticks
1658 scaling: 'linear', // => Scaling, can be 'linear' or 'logarithmic'
1660 titleAlign: 'center',
1661 margin: true // => Turn off margins with false
1667 color: '#545454', // => primary color used for outline and labels
1668 backgroundColor: null, // => null for transparent, else color
1669 backgroundImage: null, // => background image. String or object with src, left and top
1670 watermarkAlpha: 0.4, // =>
1671 tickColor: '#DDDDDD', // => color used for the ticks
1672 labelMargin: 3, // => margin in pixels
1673 verticalLines: true, // => whether to show gridlines in vertical direction
1674 minorVerticalLines: null, // => whether to show gridlines for minor ticks in vertical dir.
1675 horizontalLines: true, // => whether to show gridlines in horizontal direction
1676 minorHorizontalLines: null, // => whether to show gridlines for minor ticks in horizontal dir.
1677 outlineWidth: 1, // => width of the grid outline/border in pixels
1678 outline : 'nsew', // => walls of the outline to display
1679 circular: false // => if set to true, the grid will be circular, must be used when radars are drawn
1682 track: false, // => true to track the mouse, no tracking otherwise
1684 position: 'se', // => position of the value box (default south-east). False disables.
1685 relative: false, // => next to the mouse cursor
1686 trackFormatter: Flotr.defaultTrackFormatter, // => formats the values in the value box
1687 margin: 5, // => margin in pixels of the valuebox
1688 lineColor: '#FF3F19', // => line color of points that are drawn when mouse comes near a value of a series
1689 trackDecimals: 1, // => decimals for the track values
1690 sensibility: 2, // => the lower this number, the more precise you have to aim to show a value
1691 trackY: true, // => whether or not to track the mouse in the y axis
1692 radius: 3, // => radius of the track point
1693 fillColor: null, // => color to fill our select bar with only applies to bar and similar graphs (only bars for now)
1694 fillOpacity: 0.4 // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
1708 function Color (r, g, b, a) {
1709 this.rgba = ['r','g','b','a'];
1712 this[this.rgba[x]] = arguments[x] || ((x==3) ? 1.0 : 0);
1719 aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],
1720 brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],
1721 darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],
1722 darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],
1723 darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],
1724 khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],
1725 lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],
1726 maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],
1727 violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]
1731 scale: function(rf, gf, bf, af){
1734 if (!_.isUndefined(arguments[x])) this[this.rgba[x]] *= arguments[x];
1736 return this.normalize();
1738 alpha: function(alpha) {
1739 if (!_.isUndefined(alpha) && !_.isNull(alpha)) {
1742 return this.normalize();
1745 return new Color(this.r, this.b, this.g, this.a);
1747 limit: function(val,minVal,maxVal){
1748 return Math.max(Math.min(val, maxVal), minVal);
1750 normalize: function(){
1751 var limit = this.limit;
1752 this.r = limit(parseInt(this.r, 10), 0, 255);
1753 this.g = limit(parseInt(this.g, 10), 0, 255);
1754 this.b = limit(parseInt(this.b, 10), 0, 255);
1755 this.a = limit(this.a, 0, 1);
1758 distance: function(color){
1760 color = new Color.parse(color);
1761 var dist = 0, x = 3;
1763 dist += Math.abs(this[this.rgba[x]] - color[this.rgba[x]]);
1767 toString: function(){
1768 return (this.a >= 1.0) ? 'rgb('+[this.r,this.g,this.b].join(',')+')' : 'rgba('+[this.r,this.g,this.b,this.a].join(',')+')';
1770 contrast: function () {
1772 test = 1 - ( 0.299 * this.r + 0.587 * this.g + 0.114 * this.b) / 255;
1773 return (test < 0.5 ? '#000000' : '#ffffff');
1779 * Parses a color string and returns a corresponding Color.
1780 * The different tests are in order of probability to improve speed.
1781 * @param {String, Color} str - string thats representing a color
1782 * @return {Color} returns a Color object or false
1784 parse: function(color){
1785 if (color instanceof Color) return color;
1790 if((result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color)))
1791 return new Color(parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16));
1794 if((result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color)))
1795 return new Color(parseInt(result[1], 10), parseInt(result[2], 10), parseInt(result[3], 10));
1798 if((result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color)))
1799 return new Color(parseInt(result[1]+result[1],16), parseInt(result[2]+result[2],16), parseInt(result[3]+result[3],16));
1801 // rgba(num,num,num,num)
1802 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)))
1803 return new Color(parseInt(result[1], 10), parseInt(result[2], 10), parseInt(result[3], 10), parseFloat(result[4]));
1805 // rgb(num%,num%,num%)
1806 if((result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(color)))
1807 return new Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55);
1809 // rgba(num%,num%,num%,num)
1810 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)))
1811 return new Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55, parseFloat(result[4]));
1813 // Otherwise, we're most likely dealing with a named color.
1814 var name = (color+'').replace(/^\s*([\S\s]*?)\s*$/, '$1').toLowerCase();
1815 if(name == 'transparent'){
1816 return new Color(255, 255, 255, 0);
1818 return (result = COLOR_NAMES[name]) ? new Color(result[0], result[1], result[2]) : new Color(0, 0, 0, 0);
1822 * Process color and options into color style.
1824 processColor: function(color, options) {
1826 var opacity = options.opacity;
1827 if (!color) return 'rgba(0, 0, 0, 0)';
1828 if (color instanceof Color) return color.alpha(opacity).toString();
1829 if (_.isString(color)) return Color.parse(color).alpha(opacity).toString();
1831 var grad = color.colors ? color : {colors: color};
1834 if (!_.isArray(grad.colors)) return 'rgba(0, 0, 0, 0)';
1835 return Color.parse(_.isArray(grad.colors[0]) ? grad.colors[0][1] : grad.colors[0]).alpha(opacity).toString();
1837 grad = _.extend({start: 'top', end: 'bottom'}, grad);
1839 if (/top/i.test(grad.start)) options.x1 = 0;
1840 if (/left/i.test(grad.start)) options.y1 = 0;
1841 if (/bottom/i.test(grad.end)) options.x2 = 0;
1842 if (/right/i.test(grad.end)) options.y2 = 0;
1844 var i, c, stop, gradient = options.ctx.createLinearGradient(options.x1, options.y1, options.x2, options.y2);
1845 for (i = 0; i < grad.colors.length; i++) {
1851 else stop = i / (grad.colors.length-1);
1852 gradient.addColorStop(stop, Color.parse(c).alpha(opacity));
1858 Flotr.Color = Color;
1867 set : function (date, name, mode, value) {
1868 mode = mode || 'UTC';
1869 name = 'set' + (mode === 'UTC' ? 'UTC' : '') + name;
1873 get : function (date, name, mode) {
1874 mode = mode || 'UTC';
1875 name = 'get' + (mode === 'UTC' ? 'UTC' : '') + name;
1876 return date[name]();
1879 format: function(d, format, mode) {
1882 // We should maybe use an "official" date format spec, like PHP date() or ColdFusion
1883 // http://fr.php.net/manual/en/function.date.php
1884 // http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=functions_c-d_29.html
1888 h: get(d, 'Hours', mode).toString(),
1889 H: leftPad(get(d, 'Hours', mode)),
1890 M: leftPad(get(d, 'Minutes', mode)),
1891 S: leftPad(get(d, 'Seconds', mode)),
1892 s: get(d, 'Milliseconds', mode),
1893 d: get(d, 'Date', mode).toString(),
1894 m: (get(d, 'Month', mode) + 1).toString(),
1895 y: get(d, 'FullYear', mode).toString(),
1896 b: Flotr.Date.monthNames[get(d, 'Month', mode)]
1899 function leftPad(n){
1901 return n.length == 1 ? "0" + n : n;
1907 for (var i = 0; i < format.length; ++i) {
1908 c = format.charAt(i);
1911 r.push(tokens[c] || c);
1921 getFormat: function(time, span) {
1922 var tu = Flotr.Date.timeUnits;
1923 if (time < tu.second) return "%h:%M:%S.%s";
1924 else if (time < tu.minute) return "%h:%M:%S";
1925 else if (time < tu.day) return (span < 2 * tu.day) ? "%h:%M" : "%b %d %h:%M";
1926 else if (time < tu.month) return "%b %d";
1927 else if (time < tu.year) return (span < tu.year) ? "%b" : "%b %y";
1930 formatter: function (v, axis) {
1932 options = axis.options,
1933 scale = Flotr.Date.timeUnits[options.timeUnit],
1934 d = new Date(v * scale);
1936 // first check global format
1937 if (axis.options.timeFormat)
1938 return Flotr.Date.format(d, options.timeFormat, options.timeMode);
1940 var span = (axis.max - axis.min) * scale,
1941 t = axis.tickSize * Flotr.Date.timeUnits[axis.tickUnit];
1943 return Flotr.Date.format(d, Flotr.Date.getFormat(t, span), options.timeMode);
1945 generator: function(axis) {
1950 timeUnits = this.timeUnits,
1952 options = axis.options,
1953 mode = options.timeMode,
1954 scale = timeUnits[options.timeUnit],
1955 min = axis.min * scale,
1956 max = axis.max * scale,
1957 delta = (max - min) / options.noTicks,
1959 tickSize = axis.tickSize,
1963 // Use custom formatter or time tick formatter
1964 formatter = (options.tickFormatter === Flotr.defaultTickFormatter ?
1965 this.formatter : options.tickFormatter
1968 for (i = 0; i < spec.length - 1; ++i) {
1969 var d = spec[i][0] * timeUnits[spec[i][1]];
1970 if (delta < (d + spec[i+1][0] * timeUnits[spec[i+1][1]]) / 2 && d >= tickSize)
1973 tickSize = spec[i][0];
1974 tickUnit = spec[i][1];
1976 // special-case the possibility of several years
1977 if (tickUnit == "year") {
1978 tickSize = Flotr.getTickSize(options.noTicks*timeUnits.year, min, max, 0);
1980 // Fix for 0.5 year case
1981 if (tickSize == 0.5) {
1987 axis.tickUnit = tickUnit;
1988 axis.tickSize = tickSize;
1990 var step = tickSize * timeUnits[tickUnit];
1993 function setTick (name) {
1994 set(d, name, mode, Flotr.floorInBase(
1995 get(d, name, mode), tickSize
2000 case "millisecond": setTick('Milliseconds'); break;
2001 case "second": setTick('Seconds'); break;
2002 case "minute": setTick('Minutes'); break;
2003 case "hour": setTick('Hours'); break;
2004 case "month": setTick('Month'); break;
2005 case "year": setTick('FullYear'); break;
2008 // reset smaller components
2009 if (step >= timeUnits.second) set(d, 'Milliseconds', mode, 0);
2010 if (step >= timeUnits.minute) set(d, 'Seconds', mode, 0);
2011 if (step >= timeUnits.hour) set(d, 'Minutes', mode, 0);
2012 if (step >= timeUnits.day) set(d, 'Hours', mode, 0);
2013 if (step >= timeUnits.day * 4) set(d, 'Date', mode, 1);
2014 if (step >= timeUnits.year) set(d, 'Month', mode, 0);
2016 var carry = 0, v = NaN, prev;
2020 ticks.push({ v: v / scale, label: formatter(v / scale, axis) });
2021 if (tickUnit == "month") {
2023 /* a bit complicated - we'll divide the month up but we need to take care of fractions
2024 so we don't end up in the middle of a day */
2025 set(d, 'Date', mode, 1);
2026 var start = d.getTime();
2027 set(d, 'Month', mode, get(d, 'Month', mode) + 1);
2028 var end = d.getTime();
2029 d.setTime(v + carry * timeUnits.hour + (end - start) * tickSize);
2030 carry = get(d, 'Hours', mode);
2031 set(d, 'Hours', mode, 0);
2034 set(d, 'Month', mode, get(d, 'Month', mode) + tickSize);
2036 else if (tickUnit == "year") {
2037 set(d, 'FullYear', mode, get(d, 'FullYear', mode) + tickSize);
2040 d.setTime(v + step);
2042 } while (v < max && v != prev);
2050 hour: 1000 * 60 * 60,
2051 day: 1000 * 60 * 60 * 24,
2052 month: 1000 * 60 * 60 * 24 * 30,
2053 year: 1000 * 60 * 60 * 24 * 365.2425
2055 // the allowed tick sizes, after 1 year we use an integer algorithm
2057 [1, "millisecond"], [20, "millisecond"], [50, "millisecond"], [100, "millisecond"], [200, "millisecond"], [500, "millisecond"],
2058 [1, "second"], [2, "second"], [5, "second"], [10, "second"], [30, "second"],
2059 [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], [30, "minute"],
2060 [1, "hour"], [2, "hour"], [4, "hour"], [8, "hour"], [12, "hour"],
2061 [1, "day"], [2, "day"], [3, "day"],
2062 [0.25, "month"], [0.5, "month"], [1, "month"], [2, "month"], [3, "month"], [6, "month"],
2065 monthNames: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
2072 function getEl (el) {
2073 return (el && el.jquery) ? el[0] : el;
2077 addClass: function(element, name){
2078 element = getEl(element);
2079 var classList = (element.className ? element.className : '');
2080 if (_.include(classList.split(/\s+/g), name)) return;
2081 element.className = (classList ? classList + ' ' : '') + name;
2084 * Create an element.
2086 create: function(tag){
2087 return document.createElement(tag);
2089 node: function(html) {
2090 var div = Flotr.DOM.create('div'), n;
2091 div.innerHTML = html;
2092 n = div.children[0];
2097 * Remove all children.
2099 empty: function(element){
2100 element = getEl(element);
2101 element.innerHTML = '';
2103 if (!element) return;
2104 _.each(element.childNodes, function (e) {
2106 element.removeChild(e);
2110 remove: function (element) {
2111 element = getEl(element);
2112 element.parentNode.removeChild(element);
2114 hide: function(element){
2115 element = getEl(element);
2116 Flotr.DOM.setStyles(element, {display:'none'});
2120 * @param {Element} element
2121 * @param {Element|String} Element or string to be appended.
2123 insert: function(element, child){
2124 element = getEl(element);
2125 if(_.isString(child))
2126 element.innerHTML += child;
2127 else if (_.isElement(child))
2128 element.appendChild(child);
2130 // @TODO find xbrowser implementation
2131 opacity: function(element, opacity) {
2132 element = getEl(element);
2133 element.style.opacity = opacity;
2135 position: function(element, p){
2136 element = getEl(element);
2137 if (!element.offsetParent)
2138 return {left: (element.offsetLeft || 0), top: (element.offsetTop || 0)};
2140 p = this.position(element.offsetParent);
2141 p.left += element.offsetLeft;
2142 p.top += element.offsetTop;
2145 removeClass: function(element, name) {
2146 var classList = (element.className ? element.className : '');
2147 element = getEl(element);
2148 element.className = _.filter(classList.split(/\s+/g), function (c) {
2149 if (c != name) return true; }
2152 setStyles: function(element, o) {
2153 element = getEl(element);
2154 _.each(o, function (value, key) {
2155 element.style[key] = value;
2158 show: function(element){
2159 element = getEl(element);
2160 Flotr.DOM.setStyles(element, {display:''});
2163 * Return element size.
2165 size: function(element){
2166 element = getEl(element);
2168 height : element.offsetHeight,
2169 width : element.offsetWidth };
2176 * Flotr Event Adapter
2183 observe: function(object, name, callback) {
2184 bean.add(object, name, callback);
2187 fire: function(object, name, args) {
2188 bean.fire(object, name, args);
2189 if (typeof(Prototype) != 'undefined')
2190 Event.fire(object, name, args);
2191 // @TODO Someone who uses mootools, add mootools adapter for existing applciations.
2194 stopObserving: function(object, name, callback) {
2195 bean.remove(object, name, callback);
2198 eventPointer: function(e) {
2199 if (!F._.isUndefined(e.touches) && e.touches.length > 0) {
2201 x : e.touches[0].pageX,
2202 y : e.touches[0].pageY
2204 } else if (!F._.isUndefined(e.changedTouches) && e.changedTouches.length > 0) {
2206 x : e.changedTouches[0].pageX,
2207 y : e.changedTouches[0].pageY
2209 } else if (e.pageX || e.pageY) {
2214 } else if (e.clientX || e.clientY) {
2218 de = d.documentElement;
2220 x: e.clientX + b.scrollLeft + de.scrollLeft,
2221 y: e.clientY + b.scrollTop + de.scrollTop
2238 Text = function (o) {
2244 dimensions : function (text, canvasStyle, htmlStyle, className) {
2246 if (!text) return { width : 0, height : 0 };
2248 return (this.o.html) ?
2249 this.html(text, this.o.element, htmlStyle, className) :
2250 this.canvas(text, canvasStyle);
2253 canvas : function (text, style) {
2255 if (!this.o.textEnabled) return;
2256 style = style || {};
2259 metrics = this.measureText(text, style),
2260 width = metrics.width,
2261 height = style.size || F.defaultOptions.fontSize,
2262 angle = style.angle || 0,
2263 cosAngle = Math.cos(angle),
2264 sinAngle = Math.sin(angle),
2270 width: Math.abs(cosAngle * width) + Math.abs(sinAngle * height) + widthPadding,
2271 height: Math.abs(sinAngle * width) + Math.abs(cosAngle * height) + heightPadding
2277 html : function (text, element, style, className) {
2279 var div = D.create('div');
2281 D.setStyles(div, { 'position' : 'absolute', 'top' : '-10000px' });
2282 D.insert(div, '<div style="'+style+'" class="'+className+' flotr-dummy-div">' + text + '</div>');
2283 D.insert(this.o.element, div);
2288 measureText : function (text, style) {
2291 context = this.o.ctx,
2294 if (!context.fillText || (F.isIphone && context.measure)) {
2295 return { width : context.measure(text, style)};
2299 size: F.defaultOptions.fontSize,
2305 context.font = (style.weight > 1 ? "bold " : "") + (style.size*1.3) + "px sans-serif";
2306 metrics = context.measureText(text);
2318 * Flotr Graph class that plots a graph on creation.
2324 E = Flotr.EventAdapter,
2328 * Flotr Graph constructor.
2329 * @param {Element} el - element to insert the graph into
2330 * @param {Object} data - an array or object of dataseries
2331 * @param {Object} options - an object containing options
2333 Graph = function(el, data, options){
2334 // Let's see if we can get away with out this [JS]
2337 this._initMembers();
2338 this._initPlugins();
2340 E.fire(this.el, 'flotr:beforeinit', [this]);
2343 this.series = flotr.Series.getSeries(data);
2344 this._initOptions(options);
2345 this._initGraphTypes();
2347 this._text = new flotr.Text({
2350 html : this.options.HtmlText,
2351 textEnabled : this.textEnabled
2353 E.fire(this.el, 'flotr:afterconstruct', [this]);
2356 this.findDataRanges();
2357 this.calculateSpacing();
2359 this.draw(_.bind(function() {
2360 E.fire(this.el, 'flotr:afterinit', [this]);
2371 function observe (object, name, callback) {
2372 E.observe.apply(this, arguments);
2373 this._handles.push(arguments);
2379 destroy: function () {
2380 E.fire(this.el, 'flotr:destroy');
2381 _.each(this._handles, function (handle) {
2382 E.stopObserving.apply(this, handle);
2385 this.el.graph = null;
2395 processColor: function(color, options){
2396 var o = { x1: 0, y1: 0, x2: this.plotWidth, y2: this.plotHeight, opacity: 1, ctx: this.ctx };
2397 _.extend(o, options);
2398 return flotr.Color.processColor(color, o);
2401 * Function determines the min and max values for the xaxis and yaxis.
2403 * TODO logarithmic range validation (consideration of 0)
2405 findDataRanges: function(){
2407 xaxis, yaxis, range;
2409 _.each(this.series, function (series) {
2410 range = series.getRange();
2412 xaxis = series.xaxis;
2413 yaxis = series.yaxis;
2414 xaxis.datamin = Math.min(range.xmin, xaxis.datamin);
2415 xaxis.datamax = Math.max(range.xmax, xaxis.datamax);
2416 yaxis.datamin = Math.min(range.ymin, yaxis.datamin);
2417 yaxis.datamax = Math.max(range.ymax, yaxis.datamax);
2418 xaxis.used = (xaxis.used || range.xused);
2419 yaxis.used = (yaxis.used || range.yused);
2423 // Check for empty data, no data case (none used)
2424 if (!a.x.used && !a.x2.used) a.x.used = true;
2425 if (!a.y.used && !a.y2.used) a.y.used = true;
2427 _.each(a, function (axis) {
2428 axis.calculateRange();
2432 types = _.keys(flotr.graphTypes),
2435 _.each(this.series, function (series) {
2436 if (series.hide) return;
2437 _.each(types, function (type) {
2438 if (series[type] && series[type].show) {
2439 this.extendRange(type, series);
2444 this.extendRange(this.options.defaultType, series);
2449 extendRange : function (type, series) {
2450 if (this[type].extendRange) this[type].extendRange(series, series.data, series[type], this[type]);
2451 if (this[type].extendYRange) this[type].extendYRange(series.yaxis, series.data, series[type], this[type]);
2452 if (this[type].extendXRange) this[type].extendXRange(series.xaxis, series.data, series[type], this[type]);
2456 * Calculates axis label sizes.
2458 calculateSpacing: function(){
2461 options = this.options,
2462 series = this.series,
2463 margin = options.grid.labelMargin,
2469 maxOutset = options.grid.outlineWidth,
2472 // TODO post refactor, fix this
2473 _.each(a, function (axis) {
2474 axis.calculateTicks();
2475 axis.calculateTextDimensions(T, options);
2481 {size: options.fontSize*1.5},
2482 'font-size:1em;font-weight:bold;',
2485 this.titleHeight = dim.height;
2490 {size: options.fontSize},
2491 'font-size:smaller;',
2494 this.subtitleHeight = dim.height;
2496 for(j = 0; j < options.length; ++j){
2497 if (series[j].points.show){
2498 maxOutset = Math.max(maxOutset, series[j].points.radius + series[j].points.lineWidth/2);
2502 var p = this.plotOffset;
2503 if (x.options.margin === false) {
2507 if (x.options.margin === true) {
2508 p.bottom += (options.grid.circular ? 0 : (x.used && x.options.showLabels ? (x.maxLabel.height + margin) : 0)) +
2509 (x.used && x.options.title ? (x.titleSize.height + margin) : 0) + maxOutset;
2511 p.top += (options.grid.circular ? 0 : (x2.used && x2.options.showLabels ? (x2.maxLabel.height + margin) : 0)) +
2512 (x2.used && x2.options.title ? (x2.titleSize.height + margin) : 0) + this.subtitleHeight + this.titleHeight + maxOutset;
2514 p.bottom = x.options.margin;
2515 p.top = x.options.margin;
2517 if (y.options.margin === false) {
2521 if (y.options.margin === true) {
2522 p.left += (options.grid.circular ? 0 : (y.used && y.options.showLabels ? (y.maxLabel.width + margin) : 0)) +
2523 (y.used && y.options.title ? (y.titleSize.width + margin) : 0) + maxOutset;
2525 p.right += (options.grid.circular ? 0 : (y2.used && y2.options.showLabels ? (y2.maxLabel.width + margin) : 0)) +
2526 (y2.used && y2.options.title ? (y2.titleSize.width + margin) : 0) + maxOutset;
2528 p.left = y.options.margin;
2529 p.right = y.options.margin;
2532 p.top = Math.floor(p.top); // In order the outline not to be blured
2534 this.plotWidth = this.canvasWidth - p.left - p.right;
2535 this.plotHeight = this.canvasHeight - p.bottom - p.top;
2537 // TODO post refactor, fix this
2538 x.length = x2.length = this.plotWidth;
2539 y.length = y2.length = this.plotHeight;
2540 y.offset = y2.offset = this.plotHeight;
2547 * Draws grid, labels, series and outline.
2549 draw: function(after) {
2555 E.fire(this.el, 'flotr:beforedraw', [this.series, this]);
2557 if (this.series.length) {
2560 context.translate(this.plotOffset.left, this.plotOffset.top);
2562 for (i = 0; i < this.series.length; i++) {
2563 if (!this.series[i].hide) this.drawSeries(this.series[i]);
2570 E.fire(this.el, 'flotr:afterdraw', [this.series, this]);
2574 * Actually draws the graph.
2575 * @param {Object} series - series to draw
2577 drawSeries: function(series){
2579 function drawChart (series, typeKey) {
2580 var options = this.getOptions(series, typeKey);
2581 this[typeKey].draw(options);
2585 series = series || this.series;
2587 _.each(flotr.graphTypes, function (type, typeKey) {
2588 if (series[typeKey] && series[typeKey].show && this[typeKey]) {
2590 drawChart.call(this, series, typeKey);
2594 if (!drawn) drawChart.call(this, series, this.options.defaultType);
2597 getOptions : function (series, typeKey) {
2599 type = series[typeKey],
2600 graphType = this[typeKey],
2601 xaxis = series.xaxis,
2602 yaxis = series.yaxis,
2605 width : this.plotWidth,
2606 height : this.plotHeight,
2607 fontSize : this.options.fontSize,
2608 fontColor : this.options.fontColor,
2609 textEnabled : this.textEnabled,
2610 htmlText : this.options.HtmlText,
2611 text : this._text, // TODO Is this necessary?
2614 color : series.color,
2615 shadowSize : series.shadowSize,
2618 xInverse : xaxis.p2d,
2619 yInverse : yaxis.p2d
2622 options = flotr.merge(type, options);
2625 options.fillStyle = this.processColor(
2626 type.fillColor || series.color,
2627 {opacity: type.fillOpacity}
2633 * Calculates the coordinates from a mouse event object.
2634 * @param {Event} event - Mouse Event object.
2635 * @return {Object} Object with coordinates of the mouse.
2637 getEventPosition: function (e){
2642 de = d.documentElement,
2644 plotOffset = this.plotOffset,
2645 lastMousePos = this.lastMousePos,
2646 pointer = E.eventPointer(e),
2647 dx = pointer.x - lastMousePos.pageX,
2648 dy = pointer.y - lastMousePos.pageY,
2651 if ('ontouchstart' in this.el) {
2652 r = D.position(this.overlay);
2653 rx = pointer.x - r.left - plotOffset.left;
2654 ry = pointer.y - r.top - plotOffset.top;
2656 r = this.overlay.getBoundingClientRect();
2657 rx = e.clientX - r.left - plotOffset.left - b.scrollLeft - de.scrollLeft;
2658 ry = e.clientY - r.top - plotOffset.top - b.scrollTop - de.scrollTop;
2663 x2: axes.x2.p2d(rx),
2665 y2: axes.y2.p2d(ry),
2677 * Observes the 'click' event and fires the 'flotr:click' event.
2678 * @param {Event} event - 'click' Event object.
2680 clickHandler: function(event){
2681 if(this.ignoreClick){
2682 this.ignoreClick = false;
2683 return this.ignoreClick;
2685 E.fire(this.el, 'flotr:click', [this.getEventPosition(event), this]);
2688 * Observes mouse movement over the graph area. Fires the 'flotr:mousemove' event.
2689 * @param {Event} event - 'mousemove' Event object.
2691 mouseMoveHandler: function(event){
2692 if (this.mouseDownMoveHandler) return;
2693 var pos = this.getEventPosition(event);
2694 E.fire(this.el, 'flotr:mousemove', [event, pos, this]);
2695 this.lastMousePos = pos;
2698 * Observes the 'mousedown' event.
2699 * @param {Event} event - 'mousedown' Event object.
2701 mouseDownHandler: function (event){
2704 // @TODO Context menu?
2705 if(event.isRightClick()) {
2708 var overlay = this.overlay;
2711 function cancelContextMenu () {
2713 E.stopObserving(document, 'mousemove', cancelContextMenu);
2715 E.observe(document, 'mousemove', cancelContextMenu);
2720 if (this.mouseUpHandler) return;
2721 this.mouseUpHandler = _.bind(function (e) {
2722 E.stopObserving(document, 'mouseup', this.mouseUpHandler);
2723 E.stopObserving(document, 'mousemove', this.mouseDownMoveHandler);
2724 this.mouseDownMoveHandler = null;
2725 this.mouseUpHandler = null;
2728 E.fire(this.el, 'flotr:mouseup', [e, this]);
2730 this.mouseDownMoveHandler = _.bind(function (e) {
2731 var pos = this.getEventPosition(e);
2732 E.fire(this.el, 'flotr:mousemove', [event, pos, this]);
2733 this.lastMousePos = pos;
2735 E.observe(document, 'mouseup', this.mouseUpHandler);
2736 E.observe(document, 'mousemove', this.mouseDownMoveHandler);
2737 E.fire(this.el, 'flotr:mousedown', [event, this]);
2738 this.ignoreClick = false;
2740 drawTooltip: function(content, x, y, options) {
2741 var mt = this.getMouseTrack(),
2742 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;',
2743 p = options.position,
2745 plotOffset = this.plotOffset;
2747 if(x !== null && y !== null){
2748 if (!options.relative) { // absolute to the canvas
2749 if(p.charAt(0) == 'n') style += 'top:' + (m + plotOffset.top) + 'px;bottom:auto;';
2750 else if(p.charAt(0) == 's') style += 'bottom:' + (m + plotOffset.bottom) + 'px;top:auto;';
2751 if(p.charAt(1) == 'e') style += 'right:' + (m + plotOffset.right) + 'px;left:auto;';
2752 else if(p.charAt(1) == 'w') style += 'left:' + (m + plotOffset.left) + 'px;right:auto;';
2754 else { // relative to the mouse
2755 if(p.charAt(0) == 'n') style += 'bottom:' + (m - plotOffset.top - y + this.canvasHeight) + 'px;top:auto;';
2756 else if(p.charAt(0) == 's') style += 'top:' + (m + plotOffset.top + y) + 'px;bottom:auto;';
2757 if(p.charAt(1) == 'e') style += 'left:' + (m + plotOffset.left + x) + 'px;right:auto;';
2758 else if(p.charAt(1) == 'w') style += 'right:' + (m - plotOffset.left - x + this.canvasWidth) + 'px;left:auto;';
2761 mt.style.cssText = style;
2763 D.insert(mt, content);
2771 clip: function (ctx) {
2774 o = this.plotOffset,
2775 w = this.canvasWidth,
2776 h = this.canvasHeight;
2778 ctx = ctx || this.ctx;
2781 flotr.isIE && flotr.isIE < 9 && // IE w/o canvas
2782 !flotr.isFlashCanvas // But not flash canvas
2785 // Do not clip excanvas on overlay context
2786 // Allow hits to overflow.
2787 if (ctx === this.octx) {
2791 // Clipping for excanvas :-(
2793 ctx.fillStyle = this.processColor(this.options.ieBackgroundColor);
2794 ctx.fillRect(0, 0, w, o.top);
2795 ctx.fillRect(0, 0, o.left, h);
2796 ctx.fillRect(0, h - o.bottom, w, o.bottom);
2797 ctx.fillRect(w - o.right, 0, o.right,h);
2800 ctx.clearRect(0, 0, w, o.top);
2801 ctx.clearRect(0, 0, o.left, h);
2802 ctx.clearRect(0, h - o.bottom, w, o.bottom);
2803 ctx.clearRect(w - o.right, 0, o.right,h);
2807 _initMembers: function() {
2809 this.lastMousePos = {pageX: null, pageY: null };
2810 this.plotOffset = {left: 0, right: 0, top: 0, bottom: 0};
2811 this.ignoreClick = true;
2812 this.prevHit = null;
2815 _initGraphTypes: function() {
2816 _.each(flotr.graphTypes, function(handler, graphType){
2817 this[graphType] = flotr.clone(handler);
2821 _initEvents: function () {
2825 touchendHandler, movement, touchend;
2827 if ('ontouchstart' in el) {
2829 touchendHandler = _.bind(function (e) {
2831 E.stopObserving(document, 'touchend', touchendHandler);
2832 E.fire(el, 'flotr:mouseup', [event, this]);
2833 this.multitouches = null;
2836 this.clickHandler(e);
2840 this.observe(this.overlay, 'touchstart', _.bind(function (e) {
2843 this.ignoreClick = false;
2845 if (e.touches && e.touches.length > 1) {
2846 this.multitouches = e.touches;
2849 E.fire(el, 'flotr:mousedown', [event, this]);
2850 this.observe(document, 'touchend', touchendHandler);
2853 this.observe(this.overlay, 'touchmove', _.bind(function (e) {
2855 var pos = this.getEventPosition(e);
2857 if (this.options.preventDefault) {
2863 if (this.multitouches || (e.touches && e.touches.length > 1)) {
2864 this.multitouches = e.touches;
2867 E.fire(el, 'flotr:mousemove', [event, pos, this]);
2870 this.lastMousePos = pos;
2875 observe(this.overlay, 'mousedown', _.bind(this.mouseDownHandler, this)).
2876 observe(el, 'mousemove', _.bind(this.mouseMoveHandler, this)).
2877 observe(this.overlay, 'click', _.bind(this.clickHandler, this)).
2878 observe(el, 'mouseout', function (e) {
2879 E.fire(el, 'flotr:mouseout', e);
2885 * Initializes the canvas and it's overlay canvas element. When the browser is IE, this makes use
2886 * of excanvas. The overlay canvas is inserted for displaying interactions. After the canvas elements
2887 * are created, the elements are inserted into the container element.
2889 _initCanvas: function(){
2892 children = el.children,
2893 removedChildren = [],
2898 for (i = children.length; i--;) {
2899 child = children[i];
2900 if (!this.canvas && child.className === 'flotr-canvas') {
2901 this.canvas = child;
2902 } else if (!this.overlay && child.className === 'flotr-overlay') {
2903 this.overlay = child;
2905 removedChildren.push(child);
2908 for (i = removedChildren.length; i--;) {
2909 el.removeChild(removedChildren[i]);
2912 D.setStyles(el, {position: 'relative'}); // For positioning labels and overlay.
2914 size.width = el.clientWidth;
2915 size.height = el.clientHeight;
2917 if(size.width <= 0 || size.height <= 0 || o.resolution <= 0){
2918 throw 'Invalid dimensions for plot, width = ' + size.width + ', height = ' + size.height + ', resolution = ' + o.resolution;
2921 // Main canvas for drawing graph types
2922 this.canvas = getCanvas(this.canvas, 'canvas');
2923 // Overlay canvas for interactive features
2924 this.overlay = getCanvas(this.overlay, 'overlay');
2925 this.ctx = getContext(this.canvas);
2926 this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
2927 this.octx = getContext(this.overlay);
2928 this.octx.clearRect(0, 0, this.overlay.width, this.overlay.height);
2929 this.canvasHeight = size.height;
2930 this.canvasWidth = size.width;
2931 this.textEnabled = !!this.ctx.drawText || !!this.ctx.fillText; // Enable text functions
2933 function getCanvas(canvas, name){
2935 canvas = D.create('canvas');
2936 if (typeof FlashCanvas != "undefined" && typeof canvas.getContext === 'function') {
2937 FlashCanvas.initElement(canvas);
2938 this.isFlashCanvas = true;
2940 canvas.className = 'flotr-'+name;
2941 canvas.style.cssText = 'position:absolute;left:0px;top:0px;';
2942 D.insert(el, canvas);
2944 _.each(size, function(size, attribute){
2946 if (name == 'canvas' && canvas.getAttribute(attribute) === size) {
2949 canvas.setAttribute(attribute, size * o.resolution);
2950 canvas.style[attribute] = size + 'px';
2952 canvas.context_ = null; // Reset the ExCanvas context
2956 function getContext(canvas){
2957 if(window.G_vmlCanvasManager) window.G_vmlCanvasManager.initElement(canvas); // For ExCanvas
2958 var context = canvas.getContext('2d');
2959 if(!window.G_vmlCanvasManager) context.scale(o.resolution, o.resolution);
2964 _initPlugins: function(){
2965 // TODO Should be moved to flotr and mixed in.
2966 _.each(flotr.plugins, function(plugin, name){
2967 _.each(plugin.callbacks, function(fn, c){
2968 this.observe(this.el, c, _.bind(fn, this));
2970 this[name] = flotr.clone(plugin);
2971 _.each(this[name], function(fn, p){
2972 if (_.isFunction(fn))
2973 this[name][p] = _.bind(fn, this);
2979 * Sets options and initializes some variables and color specific values, used by the constructor.
2980 * @param {Object} opts - options object
2982 _initOptions: function(opts){
2983 var options = flotr.clone(flotr.defaultOptions);
2984 options.x2axis = _.extend(_.clone(options.xaxis), options.x2axis);
2985 options.y2axis = _.extend(_.clone(options.yaxis), options.y2axis);
2986 this.options = flotr.merge(opts || {}, options);
2988 if (this.options.grid.minorVerticalLines === null &&
2989 this.options.xaxis.scaling === 'logarithmic') {
2990 this.options.grid.minorVerticalLines = true;
2992 if (this.options.grid.minorHorizontalLines === null &&
2993 this.options.yaxis.scaling === 'logarithmic') {
2994 this.options.grid.minorHorizontalLines = true;
2997 E.fire(this.el, 'flotr:afterinitoptions', [this]);
2999 this.axes = flotr.Axis.getAxes(this.options);
3001 // Initialize some variables used throughout this function.
3002 var assignedColors = [],
3004 ln = this.series.length,
3005 neededColors = this.series.length,
3006 oc = this.options.colors,
3011 // Collect user-defined colors from series.
3012 for(i = neededColors - 1; i > -1; --i){
3013 c = this.series[i].color;
3016 if(_.isNumber(c)) assignedColors.push(c);
3017 else usedColors.push(flotr.Color.parse(c));
3021 // Calculate the number of colors that need to be generated.
3022 for(i = assignedColors.length - 1; i > -1; --i)
3023 neededColors = Math.max(neededColors, assignedColors[i] + 1);
3025 // Generate needed number of colors.
3026 for(i = 0; colors.length < neededColors;){
3027 c = (oc.length == i) ? new flotr.Color(100, 100, 100) : flotr.Color.parse(oc[i]);
3029 // Make sure each serie gets a different color.
3030 var sign = variation % 2 == 1 ? -1 : 1,
3031 factor = 1 + sign * Math.ceil(variation / 2) * 0.2;
3032 c.scale(factor, factor, factor);
3035 * @todo if we're getting too close to something else, we should probably skip this one
3039 if(++i >= oc.length){
3045 // Fill the options with the generated colors.
3046 for(i = 0, j = 0; i < ln; ++i){
3049 // Assign the color.
3051 s.color = colors[j++].toString();
3052 }else if(_.isNumber(s.color)){
3053 s.color = colors[s.color].toString();
3056 // Every series needs an axis
3057 if (!s.xaxis) s.xaxis = this.axes.x;
3058 if (s.xaxis == 1) s.xaxis = this.axes.x;
3059 else if (s.xaxis == 2) s.xaxis = this.axes.x2;
3061 if (!s.yaxis) s.yaxis = this.axes.y;
3062 if (s.yaxis == 1) s.yaxis = this.axes.y;
3063 else if (s.yaxis == 2) s.yaxis = this.axes.y2;
3065 // Apply missing options to the series.
3066 for (var t in flotr.graphTypes){
3067 s[t] = _.extend(_.clone(this.options[t]), s[t]);
3069 s.mouse = _.extend(_.clone(this.options.mouse), s.mouse);
3071 if (_.isUndefined(s.shadowSize)) s.shadowSize = this.options.shadowSize;
3075 _setEl: function(el) {
3076 if (!el) throw 'The target container doesn\'t exist';
3077 else if (el.graph instanceof Graph) el.graph.destroy();
3078 else if (!el.clientWidth) throw 'The target container must be visible';
3085 Flotr.Graph = Graph;
3090 * Flotr Axis Library
3097 LOGARITHMIC = 'logarithmic';
3101 this.orientation = 1;
3103 this.datamin = Number.MAX_VALUE;
3104 this.datamax = -Number.MAX_VALUE;
3113 setScale : function () {
3115 length = this.length,
3118 offset = this.offset,
3119 orientation = this.orientation,
3120 options = this.options,
3121 logarithmic = options.scaling === LOGARITHMIC,
3125 scale = length / (log(max, options.base) - log(min, options.base));
3127 scale = length / (max - min);
3133 this.d2p = function (dataValue) {
3134 return offset + orientation * (log(dataValue, options.base) - log(min, options.base)) * scale;
3136 this.p2d = function (pointValue) {
3137 return exp((offset + orientation * pointValue) / scale + log(min, options.base), options.base);
3140 this.d2p = function (dataValue) {
3141 return offset + orientation * (dataValue - min) * scale;
3143 this.p2d = function (pointValue) {
3144 return (offset + orientation * pointValue) / scale + min;
3149 calculateTicks : function () {
3150 var options = this.options;
3153 this.minorTicks = [];
3157 this._cleanUserTicks(options.ticks, this.ticks);
3158 this._cleanUserTicks(options.minorTicks || [], this.minorTicks);
3161 if (options.mode == 'time') {
3162 this._calculateTimeTicks();
3163 } else if (options.scaling === 'logarithmic') {
3164 this._calculateLogTicks();
3166 this._calculateTicks();
3171 _.each(this.ticks, function (tick) { tick.label += ''; });
3172 _.each(this.minorTicks, function (tick) { tick.label += ''; });
3176 * Calculates the range of an axis to apply autoscaling.
3178 calculateRange: function () {
3180 if (!this.used) return;
3184 min = o.min !== null ? o.min : axis.datamin,
3185 max = o.max !== null ? o.max : axis.datamax,
3186 margin = o.autoscaleMargin;
3188 if (o.scaling == 'logarithmic') {
3189 if (min <= 0) min = axis.datamin;
3191 // Let it widen later on
3192 if (max <= 0) max = min;
3196 var widen = max ? 0.01 : 1.00;
3197 if (o.min === null) min -= widen;
3198 if (o.max === null) max += widen;
3201 if (o.scaling === 'logarithmic') {
3202 if (min < 0) min = max / o.base; // Could be the result of widening
3204 var maxexp = Math.log(max);
3205 if (o.base != Math.E) maxexp /= Math.log(o.base);
3206 maxexp = Math.ceil(maxexp);
3208 var minexp = Math.log(min);
3209 if (o.base != Math.E) minexp /= Math.log(o.base);
3210 minexp = Math.ceil(minexp);
3212 axis.tickSize = Flotr.getTickSize(o.noTicks, minexp, maxexp, o.tickDecimals === null ? 0 : o.tickDecimals);
3214 // Try to determine a suitable amount of miniticks based on the length of a decade
3215 if (o.minorTickFreq === null) {
3216 if (maxexp - minexp > 10)
3217 o.minorTickFreq = 0;
3218 else if (maxexp - minexp > 5)
3219 o.minorTickFreq = 2;
3221 o.minorTickFreq = 5;
3224 axis.tickSize = Flotr.getTickSize(o.noTicks, min, max, o.tickDecimals);
3228 axis.max = max; //extendRange may use axis.min or axis.max, so it should be set before it is caled
3230 // Autoscaling. @todo This probably fails with log scale. Find a testcase and fix it
3231 if(o.min === null && o.autoscale){
3232 axis.min -= axis.tickSize * margin;
3233 // Make sure we don't go below zero if all values are positive.
3234 if(axis.min < 0 && axis.datamin >= 0) axis.min = 0;
3235 axis.min = axis.tickSize * Math.floor(axis.min / axis.tickSize);
3238 if(o.max === null && o.autoscale){
3239 axis.max += axis.tickSize * margin;
3240 if(axis.max > 0 && axis.datamax <= 0 && axis.datamax != axis.datamin) axis.max = 0;
3241 axis.max = axis.tickSize * Math.ceil(axis.max / axis.tickSize);
3244 if (axis.min == axis.max) axis.max = axis.min + 1;
3247 calculateTextDimensions : function (T, options) {
3253 if (this.options.showLabels) {
3254 for (i = 0; i < this.ticks.length; ++i) {
3255 length = this.ticks[i].label.length;
3256 if (length > maxLabel.length){
3257 maxLabel = this.ticks[i].label;
3262 this.maxLabel = T.dimensions(
3264 {size:options.fontSize, angle: Flotr.toRad(this.options.labelsAngle)},
3265 'font-size:smaller;',
3269 this.titleSize = T.dimensions(
3271 {size:options.fontSize*1.2, angle: Flotr.toRad(this.options.titleAngle)},
3272 'font-weight:bold;',
3277 _cleanUserTicks : function (ticks, axisTicks) {
3279 var axis = this, options = this.options,
3282 if(_.isFunction(ticks)) ticks = ticks({min : axis.min, max : axis.max});
3284 for(i = 0; i < ticks.length; ++i){
3286 if(typeof(tick) === 'object'){
3288 label = (tick.length > 1) ? tick[1] : options.tickFormatter(v, {min : axis.min, max : axis.max});
3291 label = options.tickFormatter(v, {min : this.min, max : this.max});
3293 axisTicks[i] = { v: v, label: label };
3297 _calculateTimeTicks : function () {
3298 this.ticks = Flotr.Date.generator(this);
3301 _calculateLogTicks : function () {
3308 var max = Math.log(axis.max);
3309 if (o.base != Math.E) max /= Math.log(o.base);
3310 max = Math.ceil(max);
3312 var min = Math.log(axis.min);
3313 if (o.base != Math.E) min /= Math.log(o.base);
3314 min = Math.ceil(min);
3316 for (i = min; i < max; i += axis.tickSize) {
3317 decadeStart = (o.base == Math.E) ? Math.exp(i) : Math.pow(o.base, i);
3318 // Next decade begins here:
3319 var decadeEnd = decadeStart * ((o.base == Math.E) ? Math.exp(axis.tickSize) : Math.pow(o.base, axis.tickSize));
3320 var stepSize = (decadeEnd - decadeStart) / o.minorTickFreq;
3322 axis.ticks.push({v: decadeStart, label: o.tickFormatter(decadeStart, {min : axis.min, max : axis.max})});
3323 for (v = decadeStart + stepSize; v < decadeEnd; v += stepSize)
3324 axis.minorTicks.push({v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max})});
3327 // Always show the value at the would-be start of next decade (end of this decade)
3328 decadeStart = (o.base == Math.E) ? Math.exp(i) : Math.pow(o.base, i);
3329 axis.ticks.push({v: decadeStart, label: o.tickFormatter(decadeStart, {min : axis.min, max : axis.max})});
3332 _calculateTicks : function () {
3336 tickSize = axis.tickSize,
3339 start = tickSize * Math.ceil(min / tickSize), // Round to nearest multiple of tick size.
3345 if (o.minorTickFreq)
3346 minorTickSize = tickSize / o.minorTickFreq;
3348 // Then store all possible ticks.
3349 for (i = 0; (v = v2 = start + i * tickSize) <= max; ++i){
3351 // Round (this is always needed to fix numerical instability).
3352 decimals = o.tickDecimals;
3353 if (decimals === null) decimals = 1 - Math.floor(Math.log(tickSize) / Math.LN10);
3354 if (decimals < 0) decimals = 0;
3356 v = v.toFixed(decimals);
3357 axis.ticks.push({ v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max}) });
3359 if (o.minorTickFreq) {
3360 for (j = 0; j < o.minorTickFreq && (i * tickSize + j * minorTickSize) < max; ++j) {
3361 v = v2 + j * minorTickSize;
3362 axis.minorTicks.push({ v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max}) });
3373 getAxes : function (options) {
3375 x: new Axis({options: options.xaxis, n: 1, length: this.plotWidth}),
3376 x2: new Axis({options: options.x2axis, n: 2, length: this.plotWidth}),
3377 y: new Axis({options: options.yaxis, n: 1, length: this.plotHeight, offset: this.plotHeight, orientation: -1}),
3378 y2: new Axis({options: options.y2axis, n: 2, length: this.plotHeight, offset: this.plotHeight, orientation: -1})
3387 function log (value, base) {
3388 value = Math.log(Math.max(value, Number.MIN_VALUE));
3389 if (base !== Math.E)
3390 value /= Math.log(base);
3394 function exp (value, base) {
3395 return (base === Math.E) ? Math.exp(value) : Math.pow(base, value);
3403 * Flotr Series Library
3411 function Series (o) {
3415 Series.prototype = {
3417 getRange: function () {
3421 length = data.length,
3422 xmin = Number.MAX_VALUE,
3423 ymin = Number.MAX_VALUE,
3424 xmax = -Number.MAX_VALUE,
3425 ymax = -Number.MAX_VALUE,
3430 if (length < 0 || this.hide) return false;
3432 for (i = 0; i < length; i++) {
3436 if (x < xmin) { xmin = x; xused = true; }
3437 if (x > xmax) { xmax = x; xused = true; }
3440 if (y < ymin) { ymin = y; yused = true; }
3441 if (y > ymax) { ymax = y; yused = true; }
3458 * Collects dataseries from input and parses the series into the right format. It returns an Array
3459 * of Objects each having at least the 'data' key set.
3460 * @param {Array, Object} data - Object or array of dataseries
3461 * @return {Array} Array of Objects parsed into the right format ({(...,) data: [[x1,y1], [x2,y2], ...] (, ...)})
3463 getSeries: function(data){
3464 return _.map(data, function(s){
3467 series = new Series();
3468 _.extend(series, s);
3470 series = new Series({data:s});
3477 Flotr.Series = Series;
3482 Flotr.addType('lines', {
3484 show: false, // => setting to true will show lines, false will hide
3485 lineWidth: 2, // => line width in pixels
3486 fill: false, // => true to fill the area from the line to the x axis, false for (transparent) no fill
3487 fillBorder: false, // => draw a border around the fill
3488 fillColor: null, // => fill color
3489 fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
3490 steps: false, // => draw steps
3491 stacked: false // => setting to true will show stacked lines, false will show normal lines
3499 * Draws lines series in the canvas element.
3500 * @param {Object} options
3502 draw : function (options) {
3505 context = options.context,
3506 lineWidth = options.lineWidth,
3507 shadowSize = options.shadowSize,
3511 context.lineJoin = 'round';
3515 context.lineWidth = shadowSize / 2;
3516 offset = lineWidth / 2 + context.lineWidth / 2;
3518 // @TODO do this instead with a linear gradient
3519 context.strokeStyle = "rgba(0,0,0,0.1)";
3520 this.plot(options, offset + shadowSize / 2, false);
3522 context.strokeStyle = "rgba(0,0,0,0.2)";
3523 this.plot(options, offset, false);
3526 context.lineWidth = lineWidth;
3527 context.strokeStyle = options.color;
3529 this.plot(options, 0, true);
3534 plot : function (options, shadowOffset, incStack) {
3537 context = options.context,
3538 width = options.width,
3539 height = options.height,
3540 xScale = options.xScale,
3541 yScale = options.yScale,
3542 data = options.data,
3543 stack = options.stacked ? this.stack : false,
3544 length = data.length - 1,
3549 x1, x2, y1, y2, stack1, stack2, i;
3551 if (length < 1) return;
3553 context.beginPath();
3555 for (i = 0; i < length; ++i) {
3557 // To allow empty values
3558 if (data[i][1] === null || data[i+1][1] === null) {
3560 if (i > 0 && data[i][1] !== null) {
3564 context.closePath();
3565 context.beginPath();
3571 // Zero is infinity for log scales
3572 // TODO handle zero for logarithmic
3573 // if (xa.options.scaling === 'logarithmic' && (data[i][0] <= 0 || data[i+1][0] <= 0)) continue;
3574 // if (ya.options.scaling === 'logarithmic' && (data[i][1] <= 0 || data[i+1][1] <= 0)) continue;
3576 x1 = xScale(data[i][0]);
3577 x2 = xScale(data[i+1][0]);
3579 if (start === null) start = data[i];
3582 stack1 = stack.values[data[i][0]] || 0;
3583 stack2 = stack.values[data[i+1][0]] || stack.values[data[i][0]] || 0;
3584 y1 = yScale(data[i][1] + stack1);
3585 y2 = yScale(data[i+1][1] + stack2);
3587 data[i].y0 = stack1;
3588 stack.values[data[i][0]] = data[i][1] + stack1;
3589 if (i == length-1) {
3590 data[i+1].y0 = stack2;
3591 stack.values[data[i+1][0]] = data[i+1][1] + stack2;
3595 y1 = yScale(data[i][1]);
3596 y2 = yScale(data[i+1][1]);
3600 (y1 > height && y2 > height) ||
3601 (y1 < 0 && y2 < 0) ||
3602 (x1 < 0 && x2 < 0) ||
3603 (x1 > width && x2 > width)
3606 if ((prevx != x1) || (prevy != y1 + shadowOffset)) {
3607 context.moveTo(x1, y1 + shadowOffset);
3611 prevy = y2 + shadowOffset;
3612 if (options.steps) {
3613 context.lineTo(prevx + shadowOffset / 2, y1 + shadowOffset);
3614 context.lineTo(prevx + shadowOffset / 2, prevy);
3616 context.lineTo(prevx, prevy);
3620 if (!options.fill || options.fill && !options.fillBorder) context.stroke();
3625 // TODO stacked lines
3626 if(!shadowOffset && options.fill && start){
3627 x1 = xScale(start[0]);
3628 context.fillStyle = options.fillStyle;
3629 context.lineTo(x2, zero);
3630 context.lineTo(x1, zero);
3631 context.lineTo(x1, yScale(start[1]));
3633 if (options.fillBorder) {
3639 context.closePath();
3642 // Perform any pre-render precalculations (this should be run on data first)
3643 // - Pie chart total for calculating measures
3644 // - Stacks for lines and bars
3645 // precalculate : function () {
3649 // Get any bounds after pre calculation (axis can fetch this if does not have explicit min/max)
3650 // getBounds : function () {
3652 // getMin : function () {
3654 // getMax : function () {
3658 // Padding around rendered elements
3659 // getPadding : function () {
3662 extendYRange : function (axis, data, options, lines) {
3664 var o = axis.options;
3666 // If stacked and auto-min
3667 if (options.stacked && ((!o.max && o.max !== 0) || (!o.min && o.min !== 0))) {
3672 positiveSums = lines.positiveSums || {},
3673 negativeSums = lines.negativeSums || {},
3676 for (j = 0; j < data.length; j++) {
3678 x = data[j][0] + '';
3681 if (data[j][1] > 0) {
3682 positiveSums[x] = (positiveSums[x] || 0) + data[j][1];
3683 newmax = Math.max(newmax, positiveSums[x]);
3688 negativeSums[x] = (negativeSums[x] || 0) + data[j][1];
3689 newmin = Math.min(newmin, negativeSums[x]);
3693 lines.negativeSums = negativeSums;
3694 lines.positiveSums = positiveSums;
3700 if (options.steps) {
3702 this.hit = function (options) {
3704 data = options.data,
3705 args = options.args,
3706 yScale = options.yScale,
3708 length = data.length,
3710 x = options.xInverse(mouse.relX),
3714 for (i = 0; i < length - 1; i++) {
3715 if (x >= data[i][0] && x <= data[i+1][0]) {
3716 if (Math.abs(yScale(data[i][1]) - relY) < 8) {
3720 n.seriesIndex = options.index;
3727 this.drawHit = function (options) {
3729 context = options.context,
3730 args = options.args,
3731 data = options.data,
3732 xScale = options.xScale,
3735 y = options.yScale(args.y),
3738 if (data.length - 1 > index) {
3739 x2 = options.xScale(data[index + 1][0]);
3741 context.strokeStyle = options.color;
3742 context.lineWidth = options.lineWidth;
3743 context.beginPath();
3744 context.moveTo(x, y);
3745 context.lineTo(x2, y);
3747 context.closePath();
3752 this.clearHit = function (options) {
3754 context = options.context,
3755 args = options.args,
3756 data = options.data,
3757 xScale = options.xScale,
3758 width = options.lineWidth,
3761 y = options.yScale(args.y),
3764 if (data.length - 1 > index) {
3765 x2 = options.xScale(data[index + 1][0]);
3766 context.clearRect(x - width, y - width, x2 - x + 2 * width, 2 * width);
3775 Flotr.addType('bars', {
3778 show: false, // => setting to true will show bars, false will hide
3779 lineWidth: 2, // => in pixels
3780 barWidth: 1, // => in units of the x axis
3781 fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill
3782 fillColor: null, // => fill color
3783 fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
3784 horizontal: false, // => horizontal bars (x and y inverted)
3785 stacked: false, // => stacked bar charts
3786 centered: true, // => center the bars to their x axis value
3787 topPadding: 0.1, // => top padding in percent
3788 grouped: false // => groups bars together which share x value, hit not supported.
3794 _positive : [], // Shadow
3795 _negative : [] // Shadow
3798 draw : function (options) {
3800 context = options.context;
3805 context.lineJoin = 'miter';
3806 // @TODO linewidth not interpreted the right way.
3807 context.lineWidth = options.lineWidth;
3808 context.strokeStyle = options.color;
3809 if (options.fill) context.fillStyle = options.fillStyle;
3816 plot : function (options) {
3819 data = options.data,
3820 context = options.context,
3821 shadowSize = options.shadowSize,
3822 i, geometry, left, top, width, height;
3824 if (data.length < 1) return;
3826 this.translate(context, options.horizontal);
3828 for (i = 0; i < data.length; i++) {
3830 geometry = this.getBarGeometry(data[i][0], data[i][1], options);
3831 if (geometry === null) continue;
3833 left = geometry.left;
3835 width = geometry.width;
3836 height = geometry.height;
3838 if (options.fill) context.fillRect(left, top, width, height);
3841 context.fillStyle = 'rgba(0,0,0,0.05)';
3842 context.fillRect(left + shadowSize, top + shadowSize, width, height);
3845 if (options.lineWidth) {
3846 context.strokeRect(left, top, width, height);
3851 translate : function (context, horizontal) {
3853 context.rotate(-Math.PI / 2);
3854 context.scale(-1, 1);
3858 getBarGeometry : function (x, y, options) {
3861 horizontal = options.horizontal,
3862 barWidth = options.barWidth,
3863 centered = options.centered,
3864 stack = options.stacked ? this.stack : false,
3865 lineWidth = options.lineWidth,
3866 bisection = centered ? barWidth / 2 : 0,
3867 xScale = horizontal ? options.yScale : options.xScale,
3868 yScale = horizontal ? options.xScale : options.yScale,
3869 xValue = horizontal ? y : x,
3870 yValue = horizontal ? x : y,
3872 stackValue, left, right, top, bottom;
3874 if (options.grouped) {
3875 this.current / this.groups;
3876 xValue = xValue - bisection;
3877 barWidth = barWidth / this.groups;
3878 bisection = barWidth / 2;
3879 xValue = xValue + barWidth * this.current - bisection;
3884 stackValue = yValue > 0 ? stack.positive : stack.negative;
3885 stackOffset = stackValue[xValue] || stackOffset;
3886 stackValue[xValue] = stackOffset + yValue;
3889 left = xScale(xValue - bisection);
3890 right = xScale(xValue + barWidth - bisection);
3891 top = yScale(yValue + stackOffset);
3892 bottom = yScale(stackOffset);
3894 // TODO for test passing... probably looks better without this
3895 if (bottom < 0) bottom = 0;
3898 // if (right < xa.min || left > xa.max || top < ya.min || bottom > ya.max) continue;
3900 return (x === null || y === null) ? null : {
3906 left : Math.min(left, right) - lineWidth / 2,
3907 width : Math.abs(right - left) - lineWidth,
3908 height : bottom - top
3912 hit : function (options) {
3914 data = options.data,
3915 args = options.args,
3918 x = options.xInverse(mouse.relX),
3919 y = options.yInverse(mouse.relY),
3920 hitGeometry = this.getBarGeometry(x, y, options),
3921 width = hitGeometry.width / 2,
3922 left = hitGeometry.left,
3923 height = hitGeometry.y,
3926 for (i = data.length; i--;) {
3927 geometry = this.getBarGeometry(data[i][0], data[i][1], options);
3932 (height > 0 && height < geometry.y) ||
3934 (height < 0 && height > geometry.y)
3937 (Math.abs(left - geometry.left) < width)
3942 n.seriesIndex = options.index;
3947 drawHit : function (options) {
3948 // TODO hits for stacked bars; implement using calculateStack option?
3950 context = options.context,
3951 args = options.args,
3952 geometry = this.getBarGeometry(args.x, args.y, options),
3953 left = geometry.left,
3955 width = geometry.width,
3956 height = geometry.height;
3959 context.strokeStyle = options.color;
3960 context.lineWidth = options.lineWidth;
3961 this.translate(context, options.horizontal);
3964 context.beginPath();
3965 context.moveTo(left, top + height);
3966 context.lineTo(left, top);
3967 context.lineTo(left + width, top);
3968 context.lineTo(left + width, top + height);
3970 context.fillStyle = options.fillStyle;
3974 context.closePath();
3979 clearHit: function (options) {
3981 context = options.context,
3982 args = options.args,
3983 geometry = this.getBarGeometry(args.x, args.y, options),
3984 left = geometry.left,
3985 width = geometry.width,
3987 height = geometry.height,
3988 lineWidth = 2 * options.lineWidth;
3991 this.translate(context, options.horizontal);
3994 Math.min(top, top + height) - lineWidth,
3995 width + 2 * lineWidth,
3996 Math.abs(height) + 2 * lineWidth
4001 extendXRange : function (axis, data, options, bars) {
4002 this._extendRange(axis, data, options, bars);
4003 this.groups = (this.groups + 1) || 1;
4007 extendYRange : function (axis, data, options, bars) {
4008 this._extendRange(axis, data, options, bars);
4010 _extendRange: function (axis, data, options, bars) {
4013 max = axis.options.max;
4015 if (_.isNumber(max) || _.isString(max)) return;
4020 horizontal = options.horizontal,
4021 orientation = axis.orientation,
4022 positiveSums = this.positiveSums || {},
4023 negativeSums = this.negativeSums || {},
4024 value, datum, index, j;
4027 if ((orientation == 1 && !horizontal) || (orientation == -1 && horizontal)) {
4028 if (options.centered) {
4029 newmax = Math.max(axis.datamax + options.barWidth, newmax);
4030 newmin = Math.min(axis.datamin - options.barWidth, newmin);
4034 if (options.stacked &&
4035 ((orientation == 1 && horizontal) || (orientation == -1 && !horizontal))){
4037 for (j = data.length; j--;) {
4038 value = data[j][(orientation == 1 ? 1 : 0)]+'';
4039 datum = data[j][(orientation == 1 ? 0 : 1)];
4043 positiveSums[value] = (positiveSums[value] || 0) + datum;
4044 newmax = Math.max(newmax, positiveSums[value]);
4049 negativeSums[value] = (negativeSums[value] || 0) + datum;
4050 newmin = Math.min(newmin, negativeSums[value]);
4056 if ((orientation == 1 && horizontal) || (orientation == -1 && !horizontal)) {
4057 if (options.topPadding && (axis.max === axis.datamax || (options.stacked && this.stackMax !== newmax))) {
4058 newmax += options.topPadding * (newmax - newmin);
4062 this.stackMin = newmin;
4063 this.stackMax = newmax;
4064 this.negativeSums = negativeSums;
4065 this.positiveSums = positiveSums;
4074 Flotr.addType('bubbles', {
4076 show: false, // => setting to true will show radar chart, false will hide
4077 lineWidth: 2, // => line width in pixels
4078 fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill
4079 fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
4080 baseRadius: 2 // => ratio of the radar, against the plot size
4082 draw : function (options) {
4084 context = options.context,
4085 shadowSize = options.shadowSize;
4088 context.lineWidth = options.lineWidth;
4091 context.fillStyle = 'rgba(0,0,0,0.05)';
4092 context.strokeStyle = 'rgba(0,0,0,0.05)';
4093 this.plot(options, shadowSize / 2);
4094 context.strokeStyle = 'rgba(0,0,0,0.1)';
4095 this.plot(options, shadowSize / 4);
4098 context.strokeStyle = options.color;
4099 context.fillStyle = options.fillStyle;
4104 plot : function (options, offset) {
4107 data = options.data,
4108 context = options.context,
4112 offset = offset || 0;
4114 for (i = 0; i < data.length; ++i){
4116 geometry = this.getGeometry(data[i], options);
4118 context.beginPath();
4119 context.arc(geometry.x + offset, geometry.y + offset, geometry.z, 0, 2 * Math.PI, true);
4121 if (options.fill) context.fill();
4122 context.closePath();
4125 getGeometry : function (point, options) {
4127 x : options.xScale(point[0]),
4128 y : options.yScale(point[1]),
4129 z : point[2] * options.baseRadius
4132 hit : function (options) {
4134 data = options.data,
4135 args = options.args,
4144 n.best = n.best || Number.MAX_VALUE;
4146 for (i = data.length; i--;) {
4147 geometry = this.getGeometry(data[i], options);
4149 dx = geometry.x - relX;
4150 dy = geometry.y - relY;
4151 distance = Math.sqrt(dx * dx + dy * dy);
4153 if (distance < geometry.z && geometry.z < n.best) {
4157 n.seriesIndex = options.index;
4158 n.best = geometry.z;
4162 drawHit : function (options) {
4165 context = options.context,
4166 geometry = this.getGeometry(options.data[options.args.index], options);
4169 context.lineWidth = options.lineWidth;
4170 context.fillStyle = options.fillStyle;
4171 context.strokeStyle = options.color;
4172 context.beginPath();
4173 context.arc(geometry.x, geometry.y, geometry.z, 0, 2 * Math.PI, true);
4176 context.closePath();
4179 clearHit : function (options) {
4182 context = options.context,
4183 geometry = this.getGeometry(options.data[options.args.index], options),
4184 offset = geometry.z + options.lineWidth;
4188 geometry.x - offset,
4189 geometry.y - offset,
4195 // TODO Add a hit calculation method (like pie)
4199 Flotr.addType('candles', {
4201 show: false, // => setting to true will show candle sticks, false will hide
4202 lineWidth: 1, // => in pixels
4203 wickLineWidth: 1, // => in pixels
4204 candleWidth: 0.6, // => in units of the x axis
4205 fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill
4206 upFillColor: '#00A8F0',// => up sticks fill color
4207 downFillColor: '#CB4B4B',// => down sticks fill color
4208 fillOpacity: 0.5, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
4209 barcharts: false // => draw as barcharts (not standard bars but financial barcharts)
4212 draw : function (options) {
4215 context = options.context;
4218 context.lineJoin = 'miter';
4219 context.lineCap = 'butt';
4220 // @TODO linewidth not interpreted the right way.
4221 context.lineWidth = options.wickLineWidth || options.lineWidth;
4228 plot : function (options) {
4231 data = options.data,
4232 context = options.context,
4233 xScale = options.xScale,
4234 yScale = options.yScale,
4235 width = options.candleWidth / 2,
4236 shadowSize = options.shadowSize,
4237 lineWidth = options.lineWidth,
4238 wickLineWidth = options.wickLineWidth,
4239 pixelOffset = (wickLineWidth % 2) / 2,
4242 open, high, low, close,
4243 left, right, bottom, top, bottom2, top2, reverseLines,
4246 if (data.length < 1) return;
4248 for (i = 0; i < data.length; i++) {
4255 left = xScale(x - width);
4256 right = xScale(x + width);
4257 bottom = yScale(low);
4259 bottom2 = yScale(Math.min(open, close));
4260 top2 = yScale(Math.max(open, close));
4264 if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max)
4268 color = options[open > close ? 'downFillColor' : 'upFillColor'];
4271 if (options.fill && !options.barcharts) {
4272 context.fillStyle = 'rgba(0,0,0,0.05)';
4273 context.fillRect(left + shadowSize, top2 + shadowSize, right - left, bottom2 - top2);
4275 context.globalAlpha = options.fillOpacity;
4276 context.fillStyle = color;
4277 context.fillRect(left, top2 + lineWidth, right - left, bottom2 - top2);
4281 // Draw candle outline/border, high, low.
4282 if (lineWidth || wickLineWidth) {
4284 x = Math.floor((left + right) / 2) + pixelOffset;
4286 context.strokeStyle = color;
4287 context.beginPath();
4289 if (options.barcharts) {
4290 context.moveTo(x, Math.floor(top + lineWidth));
4291 context.lineTo(x, Math.floor(bottom + lineWidth));
4293 reverseLines = open < close;
4294 context.moveTo(reverseLines ? right : left, Math.floor(top2 + lineWidth));
4295 context.lineTo(x, Math.floor(top2 + lineWidth));
4296 context.moveTo(x, Math.floor(bottom2 + lineWidth));
4297 context.lineTo(reverseLines ? left : right, Math.floor(bottom2 + lineWidth));
4299 context.strokeRect(left, top2 + lineWidth, right - left, bottom2 - top2);
4300 context.moveTo(x, Math.floor(top2 + lineWidth));
4301 context.lineTo(x, Math.floor(top + lineWidth));
4302 context.moveTo(x, Math.floor(bottom2 + lineWidth));
4303 context.lineTo(x, Math.floor(bottom + lineWidth));
4306 context.closePath();
4312 hit : function (options) {
4314 xScale = options.xScale,
4315 yScale = options.yScale,
4316 data = options.data,
4317 args = options.args,
4319 width = options.candleWidth / 2,
4323 length = data.length,
4326 left, right, top, bottom;
4328 for (i = 0; i < length; i++) {
4332 left = xScale(datum[0] - width);
4333 right = xScale(datum[0] + width);
4334 bottom = yScale(low);
4337 if (x > left && x < right && y > top && y < bottom) {
4340 n.seriesIndex = options.index;
4346 drawHit : function (options) {
4348 context = options.context;
4352 fill : !!options.fillColor,
4353 upFillColor : options.color,
4354 downFillColor : options.color,
4355 data : [options.data[options.args.index]]
4361 clearHit : function (options) {
4363 args = options.args,
4364 context = options.context,
4365 xScale = options.xScale,
4366 yScale = options.yScale,
4367 lineWidth = options.lineWidth,
4368 width = options.candleWidth / 2,
4369 bar = options.data[args.index],
4370 left = xScale(bar[0] - width) - lineWidth,
4371 right = xScale(bar[0] + width) + lineWidth,
4372 top = yScale(bar[2]),
4373 bottom = yScale(bar[3]) + lineWidth;
4374 context.clearRect(left, top, right - left, bottom - top);
4377 extendXRange: function (axis, data, options) {
4378 if (axis.options.max === null) {
4379 axis.max = Math.max(axis.datamax + 0.5, axis.max);
4380 axis.min = Math.min(axis.datamin - 0.5, axis.min);
4386 * Base on data in form [s,y,d] where:
4387 * y - executor or simply y value
4388 * s - task start value
4391 Flotr.addType('gantt', {
4393 show: false, // => setting to true will show gantt, false will hide
4394 lineWidth: 2, // => in pixels
4395 barWidth: 1, // => in units of the x axis
4396 fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill
4397 fillColor: null, // => fill color
4398 fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
4399 centered: true // => center the bars to their x axis value
4402 * Draws gantt series in the canvas element.
4403 * @param {Object} series - Series with options.gantt.show = true.
4405 draw: function(series) {
4407 bw = series.gantt.barWidth,
4408 lw = Math.min(series.gantt.lineWidth, bw);
4411 ctx.translate(this.plotOffset.left, this.plotOffset.top);
4412 ctx.lineJoin = 'miter';
4415 * @todo linewidth not interpreted the right way.
4418 ctx.strokeStyle = series.color;
4421 this.gantt.plotShadows(series, bw, 0, series.gantt.fill);
4424 if(series.gantt.fill){
4425 var color = series.gantt.fillColor || series.color;
4426 ctx.fillStyle = this.processColor(color, {opacity: series.gantt.fillOpacity});
4429 this.gantt.plot(series, bw, 0, series.gantt.fill);
4432 plot: function(series, barWidth, offset, fill){
4433 var data = series.data;
4434 if(data.length < 1) return;
4436 var xa = series.xaxis,
4440 for(i = 0; i < data.length; i++){
4444 drawLeft = true, drawTop = true, drawRight = true;
4446 if (s === null || d === null) continue;
4450 bottom = y - (series.gantt.centered ? barWidth/2 : 0),
4451 top = y + barWidth - (series.gantt.centered ? barWidth/2 : 0);
4453 if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max)
4463 if (xa.lastSerie != series)
4472 if (ya.lastSerie != series)
4481 ctx.moveTo(xa.d2p(left), ya.d2p(bottom) + offset);
4482 ctx.lineTo(xa.d2p(left), ya.d2p(top) + offset);
4483 ctx.lineTo(xa.d2p(right), ya.d2p(top) + offset);
4484 ctx.lineTo(xa.d2p(right), ya.d2p(bottom) + offset);
4490 * Draw bar outline/border.
4492 if(series.gantt.lineWidth && (drawLeft || drawRight || drawTop)){
4494 ctx.moveTo(xa.d2p(left), ya.d2p(bottom) + offset);
4496 ctx[drawLeft ?'lineTo':'moveTo'](xa.d2p(left), ya.d2p(top) + offset);
4497 ctx[drawTop ?'lineTo':'moveTo'](xa.d2p(right), ya.d2p(top) + offset);
4498 ctx[drawRight?'lineTo':'moveTo'](xa.d2p(right), ya.d2p(bottom) + offset);
4505 plotShadows: function(series, barWidth, offset){
4506 var data = series.data;
4507 if(data.length < 1) return;
4513 sw = this.options.shadowSize;
4515 for(i = 0; i < data.length; i++){
4520 if (s === null || d === null) continue;
4524 bottom = y - (series.gantt.centered ? barWidth/2 : 0),
4525 top = y + barWidth - (series.gantt.centered ? barWidth/2 : 0);
4527 if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max)
4530 if(left < xa.min) left = xa.min;
4531 if(right > xa.max) right = xa.max;
4532 if(bottom < ya.min) bottom = ya.min;
4533 if(top > ya.max) top = ya.max;
4535 var width = xa.d2p(right)-xa.d2p(left)-((xa.d2p(right)+sw <= this.plotWidth) ? 0 : sw);
4536 var height = ya.d2p(bottom)-ya.d2p(top)-((ya.d2p(bottom)+sw <= this.plotHeight) ? 0 : sw );
4538 ctx.fillStyle = 'rgba(0,0,0,0.05)';
4539 ctx.fillRect(Math.min(xa.d2p(left)+sw, this.plotWidth), Math.min(ya.d2p(top)+sw, this.plotHeight), width, height);
4542 extendXRange: function(axis) {
4543 if(axis.options.max === null){
4544 var newmin = axis.min,
4547 stackedSumsPos = {},
4548 stackedSumsNeg = {},
4551 for(i = 0; i < this.series.length; ++i){
4555 if(g.show && s.xaxis == axis) {
4556 for (j = 0; j < s.data.length; j++) {
4558 y = s.data[j][0]+'';
4559 stackedSumsPos[y] = Math.max((stackedSumsPos[y] || 0), s.data[j][1]+s.data[j][2]);
4563 for (j in stackedSumsPos) {
4564 newmax = Math.max(stackedSumsPos[j], newmax);
4568 axis.lastSerie = lastSerie;
4573 extendYRange: function(axis){
4574 if(axis.options.max === null){
4575 var newmax = Number.MIN_VALUE,
4576 newmin = Number.MAX_VALUE,
4578 stackedSumsPos = {},
4579 stackedSumsNeg = {},
4582 for(i = 0; i < this.series.length; ++i){
4586 if (g.show && !s.hide && s.yaxis == axis) {
4587 var datamax = Number.MIN_VALUE, datamin = Number.MAX_VALUE;
4588 for(j=0; j < s.data.length; j++){
4589 datamax = Math.max(datamax,s.data[j][0]);
4590 datamin = Math.min(datamin,s.data[j][0]);
4594 newmax = Math.max(datamax + 0.5, newmax);
4595 newmin = Math.min(datamin - 0.5, newmin);
4598 newmax = Math.max(datamax + 1, newmax);
4599 newmin = Math.min(datamin, newmin);
4601 // For normal horizontal bars
4602 if (g.barWidth + datamax > newmax){
4603 newmax = axis.max + g.barWidth;
4607 axis.lastSerie = lastSerie;
4610 axis.tickSize = Flotr.getTickSize(axis.options.noTicks, newmin, newmax, axis.options.tickDecimals);
4617 * Formats the marker labels.
4618 * @param {Object} obj - Marker value Object {x:..,y:..}
4619 * @return {String} Formatted marker string
4623 Flotr.defaultMarkerFormatter = function(obj){
4624 return (Math.round(obj.y*100)/100)+'';
4627 Flotr.addType('markers', {
4629 show: false, // => setting to true will show markers, false will hide
4630 lineWidth: 1, // => line width of the rectangle around the marker
4631 color: '#000000', // => text color
4632 fill: false, // => fill or not the marekers' rectangles
4633 fillColor: "#FFFFFF", // => fill color
4634 fillOpacity: 0.4, // => fill opacity
4635 stroke: false, // => draw the rectangle around the markers
4636 position: 'ct', // => the markers position (vertical align: b, m, t, horizontal align: l, c, r)
4637 verticalMargin: 0, // => the margin between the point and the text.
4638 labelFormatter: Flotr.defaultMarkerFormatter,
4639 fontSize: Flotr.defaultOptions.fontSize,
4640 stacked: false, // => true if markers should be stacked
4641 stackingType: 'b', // => define staching behavior, (b- bars like, a - area like) (see Issue 125 for details)
4642 horizontal: false // => true if markers should be horizontal (For now only in a case on horizontal stacked bars, stacks should be calculated horizontaly)
4645 // TODO test stacked markers.
4652 draw : function (options) {
4655 data = options.data,
4656 context = options.context,
4657 stack = options.stacked ? options.stack : false,
4658 stackType = options.stackingType,
4665 context.lineJoin = 'round';
4666 context.lineWidth = options.lineWidth;
4667 context.strokeStyle = 'rgba(0,0,0,0.5)';
4668 context.fillStyle = options.fillStyle;
4670 function stackPos (a, b) {
4671 stackOffsetPos = stack.negative[a] || 0;
4672 stackOffsetNeg = stack.positive[a] || 0;
4674 stack.positive[a] = stackOffsetPos + b;
4675 return stackOffsetPos + b;
4677 stack.negative[a] = stackOffsetNeg + b;
4678 return stackOffsetNeg + b;
4682 for (i = 0; i < data.length; ++i) {
4688 if (stackType == 'b') {
4689 if (options.horizontal) y = stackPos(y, x);
4690 else x = stackPos(x, y);
4691 } else if (stackType == 'a') {
4692 stackOffset = stack.values[x] || 0;
4693 stack.values[x] = stackOffset + y;
4694 y = stackOffset + y;
4698 label = options.labelFormatter({x: x, y: y, index: i, data : data});
4699 this.plot(options.xScale(x), options.yScale(y), label, options);
4703 plot: function(x, y, label, options) {
4704 var context = options.context;
4705 if (isImage(label) && !label.complete) {
4706 throw 'Marker image not loaded.';
4708 this._plot(x, y, label, options);
4712 _plot: function(x, y, label, options) {
4713 var context = options.context,
4720 dim = {height : label.height, width: label.width};
4722 dim = options.text.canvas(label);
4724 dim.width = Math.floor(dim.width+margin*2);
4725 dim.height = Math.floor(dim.height+margin*2);
4727 if (options.position.indexOf('c') != -1) left -= dim.width/2 + margin;
4728 else if (options.position.indexOf('l') != -1) left -= dim.width;
4730 if (options.position.indexOf('m') != -1) top -= dim.height/2 + margin;
4731 else if (options.position.indexOf('t') != -1) top -= dim.height + options.verticalMargin;
4732 else top += options.verticalMargin;
4734 left = Math.floor(left)+0.5;
4735 top = Math.floor(top)+0.5;
4738 context.fillRect(left, top, dim.width, dim.height);
4741 context.strokeRect(left, top, dim.width, dim.height);
4744 context.drawImage(label, parseInt(left+margin, 10), parseInt(top+margin, 10));
4746 Flotr.drawText(context, label, left+margin, top+margin, {textBaseline: 'top', textAlign: 'left', size: options.fontSize, color: options.color});
4750 function isImage (i) {
4751 return typeof i === 'object' && i.constructor && (Image ? true : i.constructor === Image);
4759 * Formats the pies labels.
4760 * @param {Object} slice - Slice object
4761 * @return {String} Formatted pie label string
4768 Flotr.defaultPieLabelFormatter = function (total, value) {
4769 return (100 * value / total).toFixed(2)+'%';
4772 Flotr.addType('pie', {
4774 show: false, // => setting to true will show bars, false will hide
4775 lineWidth: 1, // => in pixels
4776 fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill
4777 fillColor: null, // => fill color
4778 fillOpacity: 0.6, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
4779 explode: 6, // => the number of pixels the splices will be far from the center
4780 sizeRatio: 0.6, // => the size ratio of the pie relative to the plot
4781 startAngle: Math.PI/4, // => the first slice start angle
4782 labelFormatter: Flotr.defaultPieLabelFormatter,
4783 pie3D: false, // => whether to draw the pie in 3 dimenstions or not (ineffective)
4784 pie3DviewAngle: (Math.PI/2 * 0.8),
4785 pie3DspliceThickness: 20,
4786 epsilon: 0.1 // => how close do you have to get to hit empty slice
4789 draw : function (options) {
4791 // TODO 3D charts what?
4793 data = options.data,
4794 context = options.context,
4795 lineWidth = options.lineWidth,
4796 shadowSize = options.shadowSize,
4797 sizeRatio = options.sizeRatio,
4798 height = options.height,
4799 width = options.width,
4800 explode = options.explode,
4801 color = options.color,
4802 fill = options.fill,
4803 fillStyle = options.fillStyle,
4804 radius = Math.min(width, height) * sizeRatio / 2,
4807 vScale = 1,//Math.cos(series.pie.viewAngle);
4808 measure = Math.PI * 2 * value / this.total,
4809 startAngle = this.startAngle || (2 * Math.PI * options.startAngle), // TODO: this initial startAngle is already in radians (fixing will be test-unstable)
4810 endAngle = startAngle + measure,
4811 bisection = startAngle + measure / 2,
4812 label = options.labelFormatter(this.total, value),
4813 //plotTickness = Math.sin(series.pie.viewAngle)*series.pie.spliceThickness / vScale;
4814 explodeCoeff = explode + radius + 4,
4815 distX = Math.cos(bisection) * explodeCoeff,
4816 distY = Math.sin(bisection) * explodeCoeff,
4817 textAlign = distX < 0 ? 'right' : 'left',
4818 textBaseline = distY > 0 ? 'top' : 'bottom',
4823 context.translate(width / 2, height / 2);
4824 context.scale(1, vScale);
4826 x = Math.cos(bisection) * explode;
4827 y = Math.sin(bisection) * explode;
4830 if (shadowSize > 0) {
4831 this.plotSlice(x + shadowSize, y + shadowSize, radius, startAngle, endAngle, context);
4833 context.fillStyle = 'rgba(0,0,0,0.1)';
4838 this.plotSlice(x, y, radius, startAngle, endAngle, context);
4840 context.fillStyle = fillStyle;
4843 context.lineWidth = lineWidth;
4844 context.strokeStyle = color;
4848 size : options.fontSize * 1.2,
4849 color : options.fontColor,
4854 if (options.htmlText || !options.textEnabled) {
4855 divStyle = 'position:absolute;' + textBaseline + ':' + (height / 2 + (textBaseline === 'top' ? distY : -distY)) + 'px;';
4856 divStyle += textAlign + ':' + (width / 2 + (textAlign === 'right' ? -distX : distX)) + 'px;';
4857 html.push('<div style="', divStyle, '" class="flotr-grid-label">', label, '</div>');
4860 style.textAlign = textAlign;
4861 style.textBaseline = textBaseline;
4862 Flotr.drawText(context, label, distX, distY, style);
4866 if (options.htmlText || !options.textEnabled) {
4867 var div = Flotr.DOM.node('<div style="color:' + options.fontColor + '" class="flotr-labels"></div>');
4868 Flotr.DOM.insert(div, html.join(''));
4869 Flotr.DOM.insert(options.element, div);
4875 this.startAngle = endAngle;
4876 this.slices = this.slices || [];
4886 plotSlice : function (x, y, radius, startAngle, endAngle, context) {
4887 context.beginPath();
4888 context.moveTo(x, y);
4889 context.arc(x, y, radius, startAngle, endAngle, false);
4890 context.lineTo(x, y);
4891 context.closePath();
4893 hit : function (options) {
4896 data = options.data[0],
4897 args = options.args,
4898 index = options.index,
4901 slice = this.slices[index],
4902 x = mouse.relX - options.width / 2,
4903 y = mouse.relY - options.height / 2,
4904 r = Math.sqrt(x * x + y * y),
4905 theta = Math.atan(y / x),
4906 circle = Math.PI * 2,
4907 explode = slice.explode || options.explode,
4908 start = slice.start % circle,
4909 end = slice.end % circle,
4910 epsilon = options.epsilon;
4914 } else if (x > 0 && y < 0) {
4918 if (r < slice.radius + explode && r > explode) {
4920 (theta > start && theta < end) || // Normal Slice
4921 (start > end && (theta < end || theta > start)) || // First slice
4922 // TODO: Document the two cases at the end:
4923 (start === end && ((slice.start === slice.end && Math.abs(theta - start) < epsilon) || (slice.start !== slice.end && Math.abs(theta-start) > epsilon)))
4926 // TODO Decouple this from hit plugin (chart shouldn't know what n means)
4932 n.seriesIndex = index;
4933 n.fraction = data[1] / this.total;
4937 drawHit: function (options) {
4939 context = options.context,
4940 slice = this.slices[options.args.seriesIndex];
4943 context.translate(options.width / 2, options.height / 2);
4944 this.plotSlice(slice.x, slice.y, slice.radius, slice.start, slice.end, context);
4948 clearHit : function (options) {
4950 context = options.context,
4951 slice = this.slices[options.args.seriesIndex],
4952 padding = 2 * options.lineWidth,
4953 radius = slice.radius + padding;
4956 context.translate(options.width / 2, options.height / 2);
4960 2 * radius + padding,
4961 2 * radius + padding
4965 extendYRange : function (axis, data) {
4966 this.total = (this.total || 0) + data[0][1];
4972 Flotr.addType('points', {
4974 show: false, // => setting to true will show points, false will hide
4975 radius: 3, // => point radius (pixels)
4976 lineWidth: 2, // => line width in pixels
4977 fill: true, // => true to fill the points with a color, false for (transparent) no fill
4978 fillColor: '#FFFFFF', // => fill color. Null to use series color.
4979 fillOpacity: 1, // => opacity of color inside the points
4980 hitRadius: null // => override for points hit radius
4983 draw : function (options) {
4985 context = options.context,
4986 lineWidth = options.lineWidth,
4987 shadowSize = options.shadowSize;
4991 if (shadowSize > 0) {
4992 context.lineWidth = shadowSize / 2;
4994 context.strokeStyle = 'rgba(0,0,0,0.1)';
4995 this.plot(options, shadowSize / 2 + context.lineWidth / 2);
4997 context.strokeStyle = 'rgba(0,0,0,0.2)';
4998 this.plot(options, context.lineWidth / 2);
5001 context.lineWidth = options.lineWidth;
5002 context.strokeStyle = options.color;
5003 if (options.fill) context.fillStyle = options.fillStyle;
5009 plot : function (options, offset) {
5011 data = options.data,
5012 context = options.context,
5013 xScale = options.xScale,
5014 yScale = options.yScale,
5017 for (i = data.length - 1; i > -1; --i) {
5019 if (y === null) continue;
5021 x = xScale(data[i][0]);
5024 if (x < 0 || x > options.width || y < 0 || y > options.height) continue;
5026 context.beginPath();
5028 context.arc(x, y + offset, options.radius, 0, Math.PI, false);
5030 context.arc(x, y, options.radius, 0, 2 * Math.PI, true);
5031 if (options.fill) context.fill();
5034 context.closePath();
5040 Flotr.addType('radar', {
5042 show: false, // => setting to true will show radar chart, false will hide
5043 lineWidth: 2, // => line width in pixels
5044 fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill
5045 fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
5046 radiusRatio: 0.90, // => ratio of the radar, against the plot size
5047 sensibility: 2 // => the lower this number, the more precise you have to aim to show a value.
5049 draw : function (options) {
5051 context = options.context,
5052 shadowSize = options.shadowSize;
5055 context.translate(options.width / 2, options.height / 2);
5056 context.lineWidth = options.lineWidth;
5059 context.fillStyle = 'rgba(0,0,0,0.05)';
5060 context.strokeStyle = 'rgba(0,0,0,0.05)';
5061 this.plot(options, shadowSize / 2);
5062 context.strokeStyle = 'rgba(0,0,0,0.1)';
5063 this.plot(options, shadowSize / 4);
5066 context.strokeStyle = options.color;
5067 context.fillStyle = options.fillStyle;
5072 plot : function (options, offset) {
5074 data = options.data,
5075 context = options.context,
5076 radius = Math.min(options.height, options.width) * options.radiusRatio / 2,
5077 step = 2 * Math.PI / data.length,
5078 angle = -Math.PI / 2,
5081 offset = offset || 0;
5083 context.beginPath();
5084 for (i = 0; i < data.length; ++i) {
5085 ratio = data[i][1] / this.max;
5087 context[i === 0 ? 'moveTo' : 'lineTo'](
5088 Math.cos(i * step + angle) * radius * ratio + offset,
5089 Math.sin(i * step + angle) * radius * ratio + offset
5092 context.closePath();
5093 if (options.fill) context.fill();
5096 getGeometry : function (point, options) {
5098 radius = Math.min(options.height, options.width) * options.radiusRatio / 2,
5099 step = 2 * Math.PI / options.data.length,
5100 angle = -Math.PI / 2,
5101 ratio = point[1] / this.max;
5104 x : (Math.cos(point[0] * step + angle) * radius * ratio) + options.width / 2,
5105 y : (Math.sin(point[0] * step + angle) * radius * ratio) + options.height / 2
5108 hit : function (options) {
5110 args = options.args,
5119 for (var i = 0; i < n.series.length; i++) {
5120 var serie = n.series[i];
5121 var data = serie.data;
5123 for (var j = data.length; j--;) {
5124 geometry = this.getGeometry(data[j], options);
5126 dx = geometry.x - relX;
5127 dy = geometry.y - relY;
5128 distance = Math.sqrt(dx * dx + dy * dy);
5130 if (distance < options.sensibility*2) {
5140 drawHit : function (options) {
5141 var step = 2 * Math.PI / options.data.length;
5142 var angle = -Math.PI / 2;
5143 var radius = Math.min(options.height, options.width) * options.radiusRatio / 2;
5145 var s = options.args.series;
5146 var point_radius = s.points.hitRadius || s.points.radius || s.mouse.radius;
5148 var context = options.context;
5150 context.translate(options.width / 2, options.height / 2);
5152 var j = options.args.index;
5153 var ratio = options.data[j][1] / this.max;
5154 var x = Math.cos(j * step + angle) * radius * ratio;
5155 var y = Math.sin(j * step + angle) * radius * ratio;
5156 context.beginPath();
5157 context.arc(x, y, point_radius , 0, 2 * Math.PI, true);
5158 context.closePath();
5161 clearHit : function (options) {
5162 var step = 2 * Math.PI / options.data.length;
5163 var angle = -Math.PI / 2;
5164 var radius = Math.min(options.height, options.width) * options.radiusRatio / 2;
5166 var context = options.context;
5169 s = options.args.series,
5170 lw = (s.points ? s.points.lineWidth : 1);
5171 offset = (s.points.hitRadius || s.points.radius || s.mouse.radius) + lw;
5173 context.translate(options.width / 2, options.height / 2);
5175 var j = options.args.index;
5176 var ratio = options.data[j][1] / this.max;
5177 var x = Math.cos(j * step + angle) * radius * ratio;
5178 var y = Math.sin(j * step + angle) * radius * ratio;
5179 context.clearRect(x-offset,y-offset,offset*2,offset*2);
5181 extendYRange : function (axis, data) {
5182 this.max = Math.max(axis.max, this.max || -Number.MAX_VALUE);
5186 Flotr.addType('timeline', {
5197 draw : function (options) {
5200 context = options.context;
5203 context.lineJoin = 'miter';
5204 context.lineWidth = options.lineWidth;
5205 context.strokeStyle = options.color;
5206 context.fillStyle = options.fillStyle;
5213 plot : function (options) {
5216 data = options.data,
5217 context = options.context,
5218 xScale = options.xScale,
5219 yScale = options.yScale,
5220 barWidth = options.barWidth,
5221 lineWidth = options.lineWidth,
5224 Flotr._.each(data, function (timeline) {
5232 xt = Math.ceil(xScale(x)),
5233 wt = Math.ceil(xScale(x + w)) - xt,
5234 yt = Math.round(yScale(y)),
5235 ht = Math.round(yScale(y - h)) - yt,
5237 x0 = xt - lineWidth / 2,
5238 y0 = Math.round(yt - ht / 2) - lineWidth / 2;
5240 context.strokeRect(x0, y0, wt, ht);
5241 context.fillRect(x0, y0, wt, ht);
5246 extendRange : function (series) {
5252 w = series.timeline.barWidth;
5254 if (xa.options.min === null)
5255 xa.min = xa.datamin - w / 2;
5257 if (xa.options.max === null) {
5262 Flotr._.each(data, function (timeline) {
5263 max = Math.max(max, timeline[0] + timeline[2]);
5266 xa.max = max + w / 2;
5269 if (ya.options.min === null)
5270 ya.min = ya.datamin - w;
5271 if (ya.options.min === null)
5272 ya.max = ya.datamax + w;
5281 Flotr.addPlugin('crosshair', {
5283 mode: null, // => one of null, 'x', 'y' or 'xy'
5284 color: '#FF0000', // => crosshair color
5285 hideCursor: true // => hide the cursor when the crosshair is shown
5288 'flotr:mousemove': function(e, pos) {
5289 if (this.options.crosshair.mode) {
5290 this.crosshair.clearCrosshair();
5291 this.crosshair.drawCrosshair(pos);
5296 * Draws the selection box.
5298 drawCrosshair: function(pos) {
5299 var octx = this.octx,
5300 options = this.options.crosshair,
5301 plotOffset = this.plotOffset,
5302 x = plotOffset.left + Math.round(pos.relX) + 0.5,
5303 y = plotOffset.top + Math.round(pos.relY) + 0.5;
5305 if (pos.relX < 0 || pos.relY < 0 || pos.relX > this.plotWidth || pos.relY > this.plotHeight) {
5306 this.el.style.cursor = null;
5307 D.removeClass(this.el, 'flotr-crosshair');
5311 if (options.hideCursor) {
5312 this.el.style.cursor = 'none';
5313 D.addClass(this.el, 'flotr-crosshair');
5317 octx.strokeStyle = options.color;
5321 if (options.mode.indexOf('x') != -1) {
5322 octx.moveTo(x, plotOffset.top);
5323 octx.lineTo(x, plotOffset.top + this.plotHeight);
5326 if (options.mode.indexOf('y') != -1) {
5327 octx.moveTo(plotOffset.left, y);
5328 octx.lineTo(plotOffset.left + this.plotWidth, y);
5335 * Removes the selection box from the overlay canvas.
5337 clearCrosshair: function() {
5340 plotOffset = this.plotOffset,
5341 position = this.lastMousePos,
5342 context = this.octx;
5346 Math.round(position.relX) + plotOffset.left,
5353 Math.round(position.relY) + plotOffset.top,
5368 function getImage (type, canvas, context, width, height, background) {
5370 // TODO add scaling for w / h
5372 mime = 'image/'+type,
5373 data = context.getImageData(0, 0, width, height),
5374 image = new Image();
5377 context.globalCompositeOperation = 'destination-over';
5378 context.fillStyle = background;
5379 context.fillRect(0, 0, width, height);
5380 image.src = canvas.toDataURL(mime);
5383 context.clearRect(0, 0, width, height);
5384 context.putImageData(data, 0, 0);
5389 Flotr.addPlugin('download', {
5391 saveImage: function (type, width, height, replaceCanvas) {
5393 grid = this.options.grid,
5396 if (Flotr.isIE && Flotr.isIE < 9) {
5397 image = '<html><body>'+this.canvas.firstChild.innerHTML+'</body></html>';
5398 return window.open().document.write(image);
5401 if (type !== 'jpeg' && type !== 'png') return;
5404 type, this.canvas, this.ctx,
5405 this.canvasWidth, this.canvasHeight,
5406 grid && grid.backgroundColor || '#ffffff'
5409 if (_.isElement(image) && replaceCanvas) {
5410 this.download.restoreCanvas();
5411 D.hide(this.canvas);
5412 D.hide(this.overlay);
5413 D.setStyles({position: 'absolute'});
5414 D.insert(this.el, image);
5415 this.saveImageElement = image;
5417 return window.open(image.src);
5421 restoreCanvas: function() {
5422 D.show(this.canvas);
5423 D.show(this.overlay);
5424 if (this.saveImageElement) this.el.removeChild(this.saveImageElement);
5425 this.saveImageElement = null;
5433 var E = Flotr.EventAdapter,
5436 Flotr.addPlugin('graphGrid', {
5439 'flotr:beforedraw' : function () {
5440 this.graphGrid.drawGrid();
5442 'flotr:afterdraw' : function () {
5443 this.graphGrid.drawOutline();
5447 drawGrid: function(){
5451 options = this.options,
5452 grid = options.grid,
5453 verticalLines = grid.verticalLines,
5454 horizontalLines = grid.horizontalLines,
5455 minorVerticalLines = grid.minorVerticalLines,
5456 minorHorizontalLines = grid.minorHorizontalLines,
5457 plotHeight = this.plotHeight,
5458 plotWidth = this.plotWidth,
5461 if(verticalLines || minorVerticalLines ||
5462 horizontalLines || minorHorizontalLines){
5463 E.fire(this.el, 'flotr:beforegrid', [this.axes.x, this.axes.y, options, this]);
5467 ctx.strokeStyle = grid.tickColor;
5469 function circularHorizontalTicks (ticks) {
5470 for(i = 0; i < ticks.length; ++i){
5471 var ratio = ticks[i].v / a.max;
5472 for(j = 0; j <= sides; ++j){
5473 ctx[j === 0 ? 'moveTo' : 'lineTo'](
5474 Math.cos(j*coeff+angle)*radius*ratio,
5475 Math.sin(j*coeff+angle)*radius*ratio
5480 function drawGridLines (ticks, callback) {
5481 _.each(_.pluck(ticks, 'v'), function(v){
5482 // Don't show lines on upper and lower bounds.
5483 if ((v <= a.min || v >= a.max) ||
5484 (v == a.min || v == a.max) && grid.outlineWidth)
5486 callback(Math.floor(a.d2p(v)) + ctx.lineWidth/2);
5489 function drawVerticalLines (x) {
5491 ctx.lineTo(x, plotHeight);
5493 function drawHorizontalLines (y) {
5495 ctx.lineTo(plotWidth, y);
5498 if (grid.circular) {
5499 ctx.translate(this.plotOffset.left+plotWidth/2, this.plotOffset.top+plotHeight/2);
5500 var radius = Math.min(plotHeight, plotWidth)*options.radar.radiusRatio/2,
5501 sides = this.axes.x.ticks.length,
5502 coeff = 2*(Math.PI/sides),
5505 // Draw grid lines in vertical direction.
5510 if(horizontalLines){
5511 circularHorizontalTicks(a.ticks);
5513 if(minorHorizontalLines){
5514 circularHorizontalTicks(a.minorTicks);
5518 _.times(sides, function(i){
5520 ctx.lineTo(Math.cos(i*coeff+angle)*radius, Math.sin(i*coeff+angle)*radius);
5526 ctx.translate(this.plotOffset.left, this.plotOffset.top);
5528 // Draw grid background, if present in options.
5529 if(grid.backgroundColor){
5530 ctx.fillStyle = this.processColor(grid.backgroundColor, {x1: 0, y1: 0, x2: plotWidth, y2: plotHeight});
5531 ctx.fillRect(0, 0, plotWidth, plotHeight);
5537 if (verticalLines) drawGridLines(a.ticks, drawVerticalLines);
5538 if (minorVerticalLines) drawGridLines(a.minorTicks, drawVerticalLines);
5541 if (horizontalLines) drawGridLines(a.ticks, drawHorizontalLines);
5542 if (minorHorizontalLines) drawGridLines(a.minorTicks, drawHorizontalLines);
5548 if(verticalLines || minorVerticalLines ||
5549 horizontalLines || minorHorizontalLines){
5550 E.fire(this.el, 'flotr:aftergrid', [this.axes.x, this.axes.y, options, this]);
5554 drawOutline: function(){
5557 options = that.options,
5558 grid = options.grid,
5559 outline = grid.outline,
5561 backgroundImage = grid.backgroundImage,
5562 plotOffset = that.plotOffset,
5563 leftOffset = plotOffset.left,
5564 topOffset = plotOffset.top,
5565 plotWidth = that.plotWidth,
5566 plotHeight = that.plotHeight,
5567 v, img, src, left, top, globalAlpha;
5569 if (!grid.outlineWidth) return;
5573 if (grid.circular) {
5574 ctx.translate(leftOffset + plotWidth / 2, topOffset + plotHeight / 2);
5575 var radius = Math.min(plotHeight, plotWidth) * options.radar.radiusRatio / 2,
5576 sides = this.axes.x.ticks.length,
5577 coeff = 2*(Math.PI/sides),
5580 // Draw axis/grid border.
5582 ctx.lineWidth = grid.outlineWidth;
5583 ctx.strokeStyle = grid.color;
5584 ctx.lineJoin = 'round';
5586 for(i = 0; i <= sides; ++i){
5587 ctx[i === 0 ? 'moveTo' : 'lineTo'](Math.cos(i*coeff+angle)*radius, Math.sin(i*coeff+angle)*radius);
5589 //ctx.arc(0, 0, radius, 0, Math.PI*2, true);
5594 ctx.translate(leftOffset, topOffset);
5596 // Draw axis/grid border.
5597 var lw = grid.outlineWidth,
5598 orig = 0.5-lw+((lw+1)%2/2),
5602 ctx.strokeStyle = grid.color;
5603 ctx.lineJoin = 'miter';
5605 ctx.moveTo(orig, orig);
5606 plotWidth = plotWidth - (lw / 2) % 1;
5607 plotHeight = plotHeight + lw / 2;
5608 ctx[outline.indexOf('n') !== -1 ? lineTo : moveTo](plotWidth, orig);
5609 ctx[outline.indexOf('e') !== -1 ? lineTo : moveTo](plotWidth, plotHeight);
5610 ctx[outline.indexOf('s') !== -1 ? lineTo : moveTo](orig, plotHeight);
5611 ctx[outline.indexOf('w') !== -1 ? lineTo : moveTo](orig, orig);
5618 if (backgroundImage) {
5620 src = backgroundImage.src || backgroundImage;
5621 left = (parseInt(backgroundImage.left, 10) || 0) + plotOffset.left;
5622 top = (parseInt(backgroundImage.top, 10) || 0) + plotOffset.top;
5625 img.onload = function() {
5627 if (backgroundImage.alpha) ctx.globalAlpha = backgroundImage.alpha;
5628 ctx.globalCompositeOperation = 'destination-over';
5629 ctx.drawImage(img, 0, 0, img.width, img.height, left, top, plotWidth, plotHeight);
5646 S_MOUSETRACK = 'opacity:0.7;background-color:#000;color:#fff;position:absolute;padding:2px 8px;-moz-border-radius:4px;border-radius:4px;white-space:nowrap;';
5648 Flotr.addPlugin('hit', {
5650 'flotr:mousemove': function(e, pos) {
5651 this.hit.track(pos);
5653 'flotr:click': function(pos) {
5655 hit = this.hit.track(pos);
5656 if (hit && !_.isUndefined(hit.index)) pos.hit = hit;
5658 'flotr:mouseout': function(e) {
5659 if (e.relatedTarget !== this.mouseTrack) {
5660 this.hit.clearHit();
5663 'flotr:destroy': function() {
5664 if (this.options.mouse.container) {
5665 D.remove(this.mouseTrack);
5667 this.mouseTrack = null;
5670 track : function (pos) {
5671 if (this.options.mouse.track || _.any(this.series, function(s){return s.mouse && s.mouse.track;})) {
5672 return this.hit.hit(pos);
5676 * Try a method on a graph type. If the method exists, execute it.
5677 * @param {Object} series
5678 * @param {String} method Method name.
5679 * @param {Array} args Arguments applied to method.
5680 * @return executed successfully or failed.
5682 executeOnType: function(s, method, args){
5687 if (!_.isArray(s)) s = [s];
5689 function e(s, index) {
5690 _.each(_.keys(flotr.graphTypes), function (type) {
5691 if (s[type] && s[type].show && !s.hide && this[type][method]) {
5692 options = this.getOptions(s, type);
5694 options.fill = !!s.mouse.fillColor;
5695 options.fillStyle = this.processColor(s.mouse.fillColor || '#ffffff', {opacity: s.mouse.fillOpacity});
5696 options.color = s.mouse.lineColor;
5697 options.context = this.octx;
5698 options.index = index;
5700 if (args) options.args = args;
5701 this[type][method].call(this[type], options);
5711 * Updates the mouse tracking point on the overlay.
5713 drawHit: function(n){
5714 var octx = this.octx,
5717 if (s.mouse.lineColor) {
5719 octx.lineWidth = (s.points ? s.points.lineWidth : 1);
5720 octx.strokeStyle = s.mouse.lineColor;
5721 octx.fillStyle = this.processColor(s.mouse.fillColor || '#ffffff', {opacity: s.mouse.fillOpacity});
5722 octx.translate(this.plotOffset.left, this.plotOffset.top);
5724 if (!this.hit.executeOnType(s, 'drawHit', n)) {
5730 // TODO fix this (points) should move to general testable graph mixin
5731 octx.arc(xa.d2p(n.x), ya.d2p(n.y), s.points.hitRadius || s.points.radius || s.mouse.radius, 0, 2 * Math.PI, true);
5742 * Removes the mouse tracking point from the overlay.
5744 clearHit: function(){
5745 var prev = this.prevHit,
5747 plotOffset = this.plotOffset;
5749 octx.translate(plotOffset.left, plotOffset.top);
5751 if (!this.hit.executeOnType(prev.series, 'clearHit', this.prevHit)) {
5752 // TODO fix this (points) should move to general testable graph mixin
5755 lw = (s.points ? s.points.lineWidth : 1);
5756 offset = (s.points.hitRadius || s.points.radius || s.mouse.radius) + lw;
5758 prev.xaxis.d2p(prev.x) - offset,
5759 prev.yaxis.d2p(prev.y) - offset,
5764 D.hide(this.mouseTrack);
5765 this.prevHit = null;
5770 * Retrieves the nearest data point from the mouse cursor. If it's within
5771 * a certain range, draw a point on the overlay canvas and display the x and y
5772 * value of the data.
5773 * @param {Object} mouse - Object that holds the relative x and y coordinates of the cursor.
5775 hit : function (mouse) {
5778 options = this.options,
5779 prevHit = this.prevHit,
5780 closest, sensibility, dataIndex, seriesIndex, series, value, xaxis, yaxis, n;
5782 if (this.series.length === 0) return;
5784 // Nearest data element.
5785 // dist, x, y, relX, relY, absX, absY, sAngle, eAngle, fraction, mouse,
5786 // xaxis, yaxis, series, index, seriesIndex
5795 if (options.mouse.trackY &&
5796 !options.mouse.trackAll &&
5797 this.hit.executeOnType(this.series, 'hit', [mouse, n]) &&
5798 !_.isUndefined(n.seriesIndex))
5800 series = this.series[n.seriesIndex];
5802 n.mouse = series.mouse;
5803 n.xaxis = series.xaxis;
5804 n.yaxis = series.yaxis;
5807 closest = this.hit.closest(mouse);
5811 closest = options.mouse.trackY ? closest.point : closest.x;
5812 seriesIndex = closest.seriesIndex;
5813 series = this.series[seriesIndex];
5814 xaxis = series.xaxis;
5815 yaxis = series.yaxis;
5816 sensibility = 2 * series.mouse.sensibility;
5819 (options.mouse.trackAll ||
5820 (closest.distanceX < sensibility / xaxis.scale &&
5821 (!options.mouse.trackY || closest.distanceY < sensibility / yaxis.scale)))
5824 n.xaxis = series.xaxis;
5825 n.yaxis = series.yaxis;
5826 n.mouse = series.mouse;
5829 n.dist = closest.distance;
5830 n.index = closest.dataIndex;
5831 n.seriesIndex = seriesIndex;
5836 if (!prevHit || (prevHit.index !== n.index || prevHit.seriesIndex !== n.seriesIndex)) {
5837 this.hit.clearHit();
5838 if (n.series && n.mouse && n.mouse.track) {
5839 this.hit.drawMouseTrack(n);
5840 this.hit.drawHit(n);
5841 Flotr.EventAdapter.fire(this.el, 'flotr:hit', [n, this]);
5848 closest : function (mouse) {
5851 series = this.series,
5852 options = this.options,
5855 compare = Number.MAX_VALUE,
5856 compareX = Number.MAX_VALUE,
5861 distance, distanceX, distanceY,
5865 function setClosest (o) {
5866 o.distance = distance;
5867 o.distanceX = distanceX;
5868 o.distanceY = distanceY;
5876 for (i = 0; i < series.length; i++) {
5880 mouseX = serie.xaxis.p2d(relX);
5881 mouseY = serie.yaxis.p2d(relY);
5883 if (serie.hide) continue;
5885 for (j = data.length; j--;) {
5889 // Add stack offset if exists
5890 if (data[j].y0) y += data[j].y0;
5892 if (x === null || y === null) continue;
5894 // don't check if the point isn't visible in the current range
5895 if (x < serie.xaxis.min || x > serie.xaxis.max) continue;
5897 distanceX = Math.abs(x - mouseX);
5898 distanceY = Math.abs(y - mouseY);
5900 // Skip square root for speed
5901 distance = distanceX * distanceX + distanceY * distanceY;
5903 if (distance < compare) {
5905 setClosest(closest);
5908 if (distanceX < compareX) {
5909 compareX = distanceX;
5910 setClosest(closestX);
5921 drawMouseTrack : function (n) {
5926 p = n.mouse.position,
5930 elStyle = S_MOUSETRACK,
5931 mouseTrack = this.mouseTrack,
5932 plotOffset = this.plotOffset,
5933 left = plotOffset.left,
5934 right = plotOffset.right,
5935 bottom = plotOffset.bottom,
5936 top = plotOffset.top,
5937 decimals = n.mouse.trackDecimals,
5938 options = this.options,
5939 container = options.mouse.container,
5942 offset, size, content;
5946 mouseTrack = D.node('<div class="flotr-mouse-value" style="'+elStyle+'"></div>');
5947 this.mouseTrack = mouseTrack;
5948 D.insert(container || this.el, mouseTrack);
5952 if (!decimals || decimals < 0) decimals = 0;
5953 if (x && x.toFixed) x = x.toFixed(decimals);
5954 if (y && y.toFixed) y = y.toFixed(decimals);
5955 content = n.mouse.trackFormatter({
5961 fraction: n.fraction
5963 if (_.isNull(content) || _.isUndefined(content)) {
5967 mouseTrack.innerHTML = content;
5975 size = D.size(mouseTrack);
5977 offset = D.position(this.el);
5979 oLeft = offset.left;
5982 if (!n.mouse.relative) { // absolute to the canvas
5984 if (p.charAt(0) == 'n') pos += (oTop + m + top);
5985 else if (p.charAt(0) == 's') pos += (oTop - m + top + this.plotHeight - size.height);
5986 pos += 'px;bottom:auto;left:';
5987 if (p.charAt(1) == 'e') pos += (oLeft - m + left + this.plotWidth - size.width);
5988 else if (p.charAt(1) == 'w') pos += (oLeft + m + left);
5989 pos += 'px;right:auto;';
5992 } else if (s.pie && s.pie.show) {
5994 x: (this.plotWidth)/2,
5995 y: (this.plotHeight)/2
5997 radius = (Math.min(this.canvasWidth, this.canvasHeight) * s.pie.sizeRatio) / 2,
5998 bisection = n.sAngle<n.eAngle ? (n.sAngle + n.eAngle) / 2: (n.sAngle + n.eAngle + 2* Math.PI) / 2;
6000 pos += 'bottom:' + (m - top - center.y - Math.sin(bisection) * radius/2 + this.canvasHeight) + 'px;top:auto;';
6001 pos += 'left:' + (m + left + center.x + Math.cos(bisection) * radius/2) + 'px;right:auto;';
6006 if (/n/.test(p)) pos += (oTop - m + top + n.yaxis.d2p(n.y) - size.height);
6007 else pos += (oTop + m + top + n.yaxis.d2p(n.y));
6008 pos += 'px;bottom:auto;left:';
6009 if (/w/.test(p)) pos += (oLeft - m + left + n.xaxis.d2p(n.x) - size.width);
6010 else pos += (oLeft + m + left + n.xaxis.d2p(n.x));
6011 pos += 'px;right:auto;';
6015 mouseTrack.style.cssText = elStyle + pos;
6017 if (n.mouse.relative) {
6018 if (!/[ew]/.test(p)) {
6019 // Center Horizontally
6020 mouseTrack.style.left =
6021 (oLeft + left + n.xaxis.d2p(n.x) - D.size(mouseTrack).width / 2) + 'px';
6023 if (!/[ns]/.test(p)) {
6024 // Center Vertically
6025 mouseTrack.style.top =
6026 (oTop + top + n.yaxis.d2p(n.y) - D.size(mouseTrack).height / 2) + 'px';
6035 * Selection Handles Plugin
6039 * show - True enables the handles plugin.
6040 * drag - Left and Right drag handles
6041 * scroll - Scrolling handle
6045 function isLeftClick (e, type) {
6046 return (e.which ? (e.which === 1) : (e.button === 0 || e.button === 1));
6049 function boundX(x, graph) {
6050 return Math.min(Math.max(0, x), graph.plotWidth - 1);
6053 function boundY(y, graph) {
6054 return Math.min(Math.max(0, y), graph.plotHeight);
6059 E = Flotr.EventAdapter,
6063 Flotr.addPlugin('selection', {
6066 pinchOnly: null, // Only select on pinch
6067 mode: null, // => one of null, 'x', 'y' or 'xy'
6068 color: '#B6D9FF', // => selection box color
6069 fps: 20 // => frames-per-second
6073 'flotr:mouseup' : function (event) {
6076 options = this.options.selection,
6077 selection = this.selection,
6078 pointer = this.getEventPosition(event);
6080 if (!options || !options.mode) return;
6081 if (selection.interval) clearInterval(selection.interval);
6083 if (this.multitouches) {
6084 selection.updateSelection();
6086 if (!options.pinchOnly) {
6087 selection.setSelectionPos(selection.selection.second, pointer);
6089 selection.clearSelection();
6091 if(selection.selecting && selection.selectionIsSane()){
6092 selection.drawSelection();
6093 selection.fireSelectEvent();
6094 this.ignoreClick = true;
6097 'flotr:mousedown' : function (event) {
6100 options = this.options.selection,
6101 selection = this.selection,
6102 pointer = this.getEventPosition(event);
6104 if (!options || !options.mode) return;
6105 if (!options.mode || (!isLeftClick(event) && _.isUndefined(event.touches))) return;
6106 if (!options.pinchOnly) selection.setSelectionPos(selection.selection.first, pointer);
6107 if (selection.interval) clearInterval(selection.interval);
6109 this.lastMousePos.pageX = null;
6110 selection.selecting = false;
6111 selection.interval = setInterval(
6112 _.bind(selection.updateSelection, this),
6116 'flotr:destroy' : function (event) {
6117 clearInterval(this.selection.interval);
6121 // TODO This isn't used. Maybe it belongs in the draw area and fire select event methods?
6122 getArea: function() {
6125 s = this.selection.selection,
6131 x1 = a.x.p2d(s.first.x);
6132 x2 = a.x.p2d(s.second.x);
6133 y1 = a.y.p2d(s.first.y);
6134 y2 = a.y.p2d(s.second.y);
6137 x1 : Math.min(x1, x2),
6138 y1 : Math.min(y1, y2),
6139 x2 : Math.max(x1, x2),
6140 y2 : Math.max(y1, y2),
6148 selection: {first: {x: -1, y: -1}, second: {x: -1, y: -1}},
6149 prevSelection: null,
6153 * Fires the 'flotr:select' event when the user made a selection.
6155 fireSelectEvent: function(name){
6157 area = this.selection.getArea();
6158 name = name || 'select';
6159 area.selection = this.selection.selection;
6160 E.fire(this.el, 'flotr:'+name, [area, this]);
6164 * Allows the user the manually select an area.
6165 * @param {Object} area - Object with coordinates to select.
6167 setSelection: function(area, preventEvent){
6168 var options = this.options,
6171 vertScale = ya.scale,
6172 hozScale = xa.scale,
6173 selX = options.selection.mode.indexOf('x') != -1,
6174 selY = options.selection.mode.indexOf('y') != -1,
6175 s = this.selection.selection;
6177 this.selection.clearSelection();
6179 s.first.y = boundY((selX && !selY) ? 0 : (ya.max - area.y1) * vertScale, this);
6180 s.second.y = boundY((selX && !selY) ? this.plotHeight - 1: (ya.max - area.y2) * vertScale, this);
6181 s.first.x = boundX((selY && !selX) ? 0 : (area.x1 - xa.min) * hozScale, this);
6182 s.second.x = boundX((selY && !selX) ? this.plotWidth : (area.x2 - xa.min) * hozScale, this);
6184 this.selection.drawSelection();
6186 this.selection.fireSelectEvent();
6190 * Calculates the position of the selection.
6191 * @param {Object} pos - Position object.
6192 * @param {Event} event - Event object.
6194 setSelectionPos: function(pos, pointer) {
6195 var mode = this.options.selection.mode,
6196 selection = this.selection.selection;
6198 if(mode.indexOf('x') == -1) {
6199 pos.x = (pos == selection.first) ? 0 : this.plotWidth;
6201 pos.x = boundX(pointer.relX, this);
6204 if (mode.indexOf('y') == -1) {
6205 pos.y = (pos == selection.first) ? 0 : this.plotHeight - 1;
6207 pos.y = boundY(pointer.relY, this);
6211 * Draws the selection box.
6213 drawSelection: function() {
6215 this.selection.fireSelectEvent('selecting');
6217 var s = this.selection.selection,
6219 options = this.options,
6220 plotOffset = this.plotOffset,
6221 prevSelection = this.selection.prevSelection;
6223 if (prevSelection &&
6224 s.first.x == prevSelection.first.x &&
6225 s.first.y == prevSelection.first.y &&
6226 s.second.x == prevSelection.second.x &&
6227 s.second.y == prevSelection.second.y) {
6232 octx.strokeStyle = this.processColor(options.selection.color, {opacity: 0.8});
6234 octx.lineJoin = 'miter';
6235 octx.fillStyle = this.processColor(options.selection.color, {opacity: 0.4});
6237 this.selection.prevSelection = {
6238 first: { x: s.first.x, y: s.first.y },
6239 second: { x: s.second.x, y: s.second.y }
6242 var x = Math.min(s.first.x, s.second.x),
6243 y = Math.min(s.first.y, s.second.y),
6244 w = Math.abs(s.second.x - s.first.x),
6245 h = Math.abs(s.second.y - s.first.y);
6247 octx.fillRect(x + plotOffset.left+0.5, y + plotOffset.top+0.5, w, h);
6248 octx.strokeRect(x + plotOffset.left+0.5, y + plotOffset.top+0.5, w, h);
6253 * Updates (draws) the selection box.
6255 updateSelection: function(){
6256 if (!this.lastMousePos.pageX) return;
6258 this.selection.selecting = true;
6260 if (this.multitouches) {
6261 this.selection.setSelectionPos(this.selection.selection.first, this.getEventPosition(this.multitouches[0]));
6262 this.selection.setSelectionPos(this.selection.selection.second, this.getEventPosition(this.multitouches[1]));
6264 if (this.options.selection.pinchOnly) {
6267 this.selection.setSelectionPos(this.selection.selection.second, this.lastMousePos);
6270 this.selection.clearSelection();
6272 if(this.selection.selectionIsSane()) {
6273 this.selection.drawSelection();
6278 * Removes the selection box from the overlay canvas.
6280 clearSelection: function() {
6281 if (!this.selection.prevSelection) return;
6283 var prevSelection = this.selection.prevSelection,
6285 plotOffset = this.plotOffset,
6286 x = Math.min(prevSelection.first.x, prevSelection.second.x),
6287 y = Math.min(prevSelection.first.y, prevSelection.second.y),
6288 w = Math.abs(prevSelection.second.x - prevSelection.first.x),
6289 h = Math.abs(prevSelection.second.y - prevSelection.first.y);
6291 this.octx.clearRect(x + plotOffset.left - lw + 0.5,
6292 y + plotOffset.top - lw,
6296 this.selection.prevSelection = null;
6299 * Determines whether or not the selection is sane and should be drawn.
6300 * @return {Boolean} - True when sane, false otherwise.
6302 selectionIsSane: function(){
6303 var s = this.selection.selection;
6304 return Math.abs(s.second.x - s.first.x) >= 5 ||
6305 Math.abs(s.second.y - s.first.y) >= 5;
6316 Flotr.addPlugin('labels', {
6319 'flotr:afterdraw' : function () {
6325 // Construct fixed width label boxes, which can be styled easily.
6327 axis, tick, left, top, xBoxWidth,
6328 radius, sides, coeff, angle,
6331 options = this.options,
6334 style = { size: options.fontSize };
6336 for (i = 0; i < a.x.ticks.length; ++i){
6337 if (a.x.ticks[i].label) { ++noLabels; }
6339 xBoxWidth = this.plotWidth / noLabels;
6341 if (options.grid.circular) {
6343 ctx.translate(this.plotOffset.left + this.plotWidth / 2,
6344 this.plotOffset.top + this.plotHeight / 2);
6346 radius = this.plotHeight * options.radar.radiusRatio / 2 + options.fontSize;
6347 sides = this.axes.x.ticks.length;
6348 coeff = 2 * (Math.PI / sides);
6349 angle = -Math.PI / 2;
6351 drawLabelCircular(this, a.x, false);
6352 drawLabelCircular(this, a.x, true);
6353 drawLabelCircular(this, a.y, false);
6354 drawLabelCircular(this, a.y, true);
6358 if (!options.HtmlText && this.textEnabled) {
6359 drawLabelNoHtmlText(this, a.x, 'center', 'top');
6360 drawLabelNoHtmlText(this, a.x2, 'center', 'bottom');
6361 drawLabelNoHtmlText(this, a.y, 'right', 'middle');
6362 drawLabelNoHtmlText(this, a.y2, 'left', 'middle');
6365 a.x.options.showLabels ||
6366 a.x2.options.showLabels ||
6367 a.y.options.showLabels ||
6368 a.y2.options.showLabels) &&
6369 !options.grid.circular
6374 drawLabelHtml(this, a.x);
6375 drawLabelHtml(this, a.x2);
6376 drawLabelHtml(this, a.y);
6377 drawLabelHtml(this, a.y2);
6381 div = D.create('div');
6383 fontSize: 'smaller',
6384 color: options.grid.color
6386 div.className = 'flotr-labels';
6387 D.insert(this.el, div);
6388 D.insert(div, html);
6391 function drawLabelCircular (graph, axis, minorTicks) {
6393 ticks = minorTicks ? axis.minorTicks : axis.ticks,
6394 isX = axis.orientation === 1,
6395 isFirst = axis.n === 1,
6399 color : axis.options.color || options.grid.color,
6400 angle : Flotr.toRad(axis.options.labelsAngle),
6401 textBaseline : 'middle'
6404 for (i = 0; i < ticks.length &&
6405 (minorTicks ? axis.options.showMinorLabels : axis.options.showLabels); ++i){
6408 if (!tick.label || !tick.label.length) { continue; }
6410 x = Math.cos(i * coeff + angle) * radius;
6411 y = Math.sin(i * coeff + angle) * radius;
6413 style.textAlign = isX ? (Math.abs(x) < 0.1 ? 'center' : (x < 0 ? 'right' : 'left')) : 'left';
6418 isX ? y : -(axis.ticks[i].v / axis.max) * (radius - options.fontSize),
6424 function drawLabelNoHtmlText (graph, axis, textAlign, textBaseline) {
6426 isX = axis.orientation === 1,
6427 isFirst = axis.n === 1,
6431 color : axis.options.color || options.grid.color,
6432 textAlign : textAlign,
6433 textBaseline : textBaseline,
6434 angle : Flotr.toRad(axis.options.labelsAngle)
6436 style = Flotr.getBestTextAlign(style.angle, style);
6438 for (i = 0; i < axis.ticks.length && continueShowingLabels(axis); ++i) {
6440 tick = axis.ticks[i];
6441 if (!tick.label || !tick.label.length) { continue; }
6443 offset = axis.d2p(tick.v);
6445 offset > (isX ? graph.plotWidth : graph.plotHeight)) { continue; }
6449 leftOffset(graph, isX, isFirst, offset),
6450 topOffset(graph, isX, isFirst, offset),
6454 // Only draw on axis y2
6455 if (!isX && !isFirst) {
6457 ctx.strokeStyle = style.color;
6459 ctx.moveTo(graph.plotOffset.left + graph.plotWidth - 8, graph.plotOffset.top + axis.d2p(tick.v));
6460 ctx.lineTo(graph.plotOffset.left + graph.plotWidth, graph.plotOffset.top + axis.d2p(tick.v));
6466 function continueShowingLabels (axis) {
6467 return axis.options.showLabels && axis.used;
6469 function leftOffset (graph, isX, isFirst, offset) {
6470 return graph.plotOffset.left +
6473 -options.grid.labelMargin :
6474 options.grid.labelMargin + graph.plotWidth));
6476 function topOffset (graph, isX, isFirst, offset) {
6477 return graph.plotOffset.top +
6478 (isX ? options.grid.labelMargin : offset) +
6479 ((isX && isFirst) ? graph.plotHeight : 0);
6483 function drawLabelHtml (graph, axis) {
6485 isX = axis.orientation === 1,
6486 isFirst = axis.n === 1,
6489 offset = graph.plotOffset;
6491 if (!isX && !isFirst) {
6493 ctx.strokeStyle = axis.options.color || options.grid.color;
6497 if (axis.options.showLabels && (isFirst ? true : axis.used)) {
6498 for (i = 0; i < axis.ticks.length; ++i) {
6499 tick = axis.ticks[i];
6500 if (!tick.label || !tick.label.length ||
6501 ((isX ? offset.left : offset.top) + axis.d2p(tick.v) < 0) ||
6502 ((isX ? offset.left : offset.top) + axis.d2p(tick.v) > (isX ? graph.canvasWidth : graph.canvasHeight))) {
6507 ((isFirst ? 1 : -1 ) * (graph.plotHeight + options.grid.labelMargin)) :
6508 axis.d2p(tick.v) - axis.maxLabel.height / 2);
6509 left = isX ? (offset.left + axis.d2p(tick.v) - xBoxWidth / 2) : 0;
6514 } else if (i === axis.ticks.length - 1) {
6517 name += isX ? ' flotr-grid-label-x' : ' flotr-grid-label-y';
6520 '<div style="position:absolute; text-align:' + (isX ? 'center' : 'right') + '; ',
6521 'top:' + top + 'px; ',
6522 ((!isX && !isFirst) ? 'right:' : 'left:') + left + 'px; ',
6523 'width:' + (isX ? xBoxWidth : ((isFirst ? offset.left : offset.right) - options.grid.labelMargin)) + 'px; ',
6524 axis.options.color ? ('color:' + axis.options.color + '; ') : ' ',
6525 '" class="flotr-grid-label' + name + '">' + tick.label + '</div>'
6528 if (!isX && !isFirst) {
6529 ctx.moveTo(offset.left + graph.plotWidth - 8, offset.top + axis.d2p(tick.v));
6530 ctx.lineTo(offset.left + graph.plotWidth, offset.top + axis.d2p(tick.v));
6546 Flotr.addPlugin('legend', {
6548 show: true, // => setting to true will show the legend, hide otherwise
6549 noColumns: 1, // => number of colums in legend table // @todo: doesn't work for HtmlText = false
6550 labelFormatter: function(v){return v;}, // => fn: string -> string
6551 labelBoxBorderColor: '#CCCCCC', // => border color for the little label boxes
6555 container: null, // => container (as jQuery object) to put legend in, null means default on top of graph
6556 position: 'nw', // => position of default legend container within plot
6557 margin: 5, // => distance from grid edge to default legend container within plot
6558 backgroundColor: '#F0F0F0', // => Legend background color.
6559 backgroundOpacity: 0.85// => set to 0 to avoid background, set to 1 for a solid background
6562 'flotr:afterinit': function() {
6563 this.legend.insertLegend();
6565 'flotr:destroy': function() {
6566 var markup = this.legend.markup;
6568 this.legend.markup = null;
6574 * Adds a legend div to the canvas container or draws it on the canvas.
6576 insertLegend: function(){
6578 if(!this.options.legend.show)
6581 var series = this.series,
6582 plotOffset = this.plotOffset,
6583 options = this.options,
6584 legend = options.legend,
6588 itemCount = _.filter(series, function(s) {return (s.label && !s.hide);}).length,
6589 p = legend.position,
6591 opacity = legend.backgroundOpacity,
6596 var lbw = legend.labelBoxWidth,
6597 lbh = legend.labelBoxHeight,
6598 lbm = legend.labelBoxMargin,
6599 offsetX = plotOffset.left + m,
6600 offsetY = plotOffset.top + m,
6603 size: options.fontSize*1.1,
6604 color: options.grid.color
6607 // We calculate the labels' max width
6608 for(i = series.length - 1; i > -1; --i){
6609 if(!series[i].label || series[i].hide) continue;
6610 label = legend.labelFormatter(series[i].label);
6611 labelMaxWidth = Math.max(labelMaxWidth, this._text.measureText(label, style).width);
6614 var legendWidth = Math.round(lbw + lbm*3 + labelMaxWidth),
6615 legendHeight = Math.round(itemCount*(lbm+lbh) + lbm);
6618 if (!opacity && opacity !== 0) {
6622 if (!options.HtmlText && this.textEnabled && !legend.container) {
6624 if(p.charAt(0) == 's') offsetY = plotOffset.top + this.plotHeight - (m + legendHeight);
6625 if(p.charAt(0) == 'c') offsetY = plotOffset.top + (this.plotHeight/2) - (m + (legendHeight/2));
6626 if(p.charAt(1) == 'e') offsetX = plotOffset.left + this.plotWidth - (m + legendWidth);
6629 color = this.processColor(legend.backgroundColor, { opacity : opacity });
6631 ctx.fillStyle = color;
6632 ctx.fillRect(offsetX, offsetY, legendWidth, legendHeight);
6633 ctx.strokeStyle = legend.labelBoxBorderColor;
6634 ctx.strokeRect(Flotr.toPixel(offsetX), Flotr.toPixel(offsetY), legendWidth, legendHeight);
6637 var x = offsetX + lbm;
6638 var y = offsetY + lbm;
6639 for(i = 0; i < series.length; i++){
6640 if(!series[i].label || series[i].hide) continue;
6641 label = legend.labelFormatter(series[i].label);
6643 ctx.fillStyle = series[i].color;
6644 ctx.fillRect(x, y, lbw-1, lbh-1);
6646 ctx.strokeStyle = legend.labelBoxBorderColor;
6648 ctx.strokeRect(Math.ceil(x)-1.5, Math.ceil(y)-1.5, lbw+2, lbh+2);
6651 Flotr.drawText(ctx, label, x + lbw + lbm, y + lbh, style);
6657 for(i = 0; i < series.length; ++i){
6658 if(!series[i].label || series[i].hide) continue;
6660 if(i % legend.noColumns === 0){
6661 fragments.push(rowStarted ? '</tr><tr>' : '<tr>');
6666 boxWidth = legend.labelBoxWidth,
6667 boxHeight = legend.labelBoxHeight;
6669 label = legend.labelFormatter(s.label);
6670 color = 'background-color:' + ((s.bars && s.bars.show && s.bars.fillColor && s.bars.fill) ? s.bars.fillColor : s.color) + ';';
6673 '<td class="flotr-legend-color-box">',
6674 '<div style="border:1px solid ', legend.labelBoxBorderColor, ';padding:1px">',
6675 '<div style="width:', (boxWidth-1), 'px;height:', (boxHeight-1), 'px;border:1px solid ', series[i].color, '">', // Border
6676 '<div style="width:', boxWidth, 'px;height:', boxHeight, 'px;', color, '"></div>', // Background
6680 '<td class="flotr-legend-label">', label, '</td>'
6683 if(rowStarted) fragments.push('</tr>');
6685 if(fragments.length > 0){
6686 var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join('') + '</table>';
6687 if(legend.container){
6688 table = D.node(table);
6689 this.legend.markup = table;
6690 D.insert(legend.container, table);
6693 var styles = {position: 'absolute', 'zIndex': '2', 'border' : '1px solid ' + legend.labelBoxBorderColor};
6695 if(p.charAt(0) == 'n') { styles.top = (m + plotOffset.top) + 'px'; styles.bottom = 'auto'; }
6696 else if(p.charAt(0) == 'c') { styles.top = (m + (this.plotHeight - legendHeight) / 2) + 'px'; styles.bottom = 'auto'; }
6697 else if(p.charAt(0) == 's') { styles.bottom = (m + plotOffset.bottom) + 'px'; styles.top = 'auto'; }
6698 if(p.charAt(1) == 'e') { styles.right = (m + plotOffset.right) + 'px'; styles.left = 'auto'; }
6699 else if(p.charAt(1) == 'w') { styles.left = (m + plotOffset.left) + 'px'; styles.right = 'auto'; }
6701 var div = D.create('div'), size;
6702 div.className = 'flotr-legend';
6703 D.setStyles(div, styles);
6704 D.insert(div, table);
6705 D.insert(this.el, div);
6707 if (!opacity) return;
6709 var c = legend.backgroundColor || options.grid.backgroundColor || '#ffffff';
6711 _.extend(styles, D.size(div), {
6712 'backgroundColor': c,
6716 styles.width += 'px';
6717 styles.height += 'px';
6719 // Put in the transparent background separately to avoid blended labels and
6720 div = D.create('div');
6721 div.className = 'flotr-legend-bg';
6722 D.setStyles(div, styles);
6723 D.opacity(div, opacity);
6725 D.insert(this.el, div);
6737 function getRowLabel(value){
6738 if (this.options.spreadsheet.tickFormatter){
6739 //TODO maybe pass the xaxis formatter to the custom tick formatter as an opt-out?
6740 return this.options.spreadsheet.tickFormatter(value);
6743 var t = _.find(this.axes.x.ticks, function(t){return t.v == value;});
6755 Flotr.addPlugin('spreadsheet', {
6757 show: false, // => show the data grid using two tabs
6758 tabGraphLabel: 'Graph',
6759 tabDataLabel: 'Data',
6760 toolbarDownload: 'Download CSV', // @todo: add better language support
6761 toolbarSelectAll: 'Select all',
6762 csvFileSeparator: ',',
6763 decimalSeparator: '.',
6764 tickFormatter: null,
6768 * Builds the tabs in the DOM
6771 'flotr:afterconstruct': function(){
6773 //this.el.select('.flotr-tabs-group,.flotr-datagrid-container').invoke('remove');
6775 if (!this.options.spreadsheet.show) return;
6777 var ss = this.spreadsheet,
6778 container = D.node('<div class="flotr-tabs-group" style="position:absolute;left:0px;width:'+this.canvasWidth+'px"></div>'),
6779 graph = D.node('<div style="float:left" class="flotr-tab selected">'+this.options.spreadsheet.tabGraphLabel+'</div>'),
6780 data = D.node('<div style="float:left" class="flotr-tab">'+this.options.spreadsheet.tabDataLabel+'</div>'),
6783 ss.tabsContainer = container;
6784 ss.tabs = { graph : graph, data : data };
6786 D.insert(container, graph);
6787 D.insert(container, data);
6788 D.insert(this.el, container);
6790 offset = D.size(data).height + 2;
6791 this.plotOffset.bottom += offset;
6793 D.setStyles(container, {top: this.canvasHeight-offset+'px'});
6796 observe(graph, 'click', function(){ss.showTab('graph');}).
6797 observe(data, 'click', function(){ss.showTab('data');});
6798 if (this.options.spreadsheet.initialTab !== 'graph'){
6799 ss.showTab(this.options.spreadsheet.initialTab);
6804 * Builds a matrix of the data to make the correspondance between the x values and the y values :
6805 * X value => Y values from the axes
6806 * @return {Array} The data grid
6808 loadDataGrid: function(){
6809 if (this.seriesData) return this.seriesData;
6811 var s = this.series,
6814 /* The data grid is a 2 dimensions array. There is a row for each X value.
6815 * Each row contains the x value and the corresponding y value for each serie ('undefined' if there isn't one)
6817 _.each(s, function(serie, i){
6818 _.each(serie.data, function (v) {
6833 // The data grid is sorted by x value
6834 this.seriesData = _.sortBy(rows, function(row, x){
6835 return parseInt(x, 10);
6837 return this.seriesData;
6840 * Constructs the data table for the spreadsheet
6841 * @todo make a spreadsheet manager (Flotr.Spreadsheet)
6842 * @return {Element} The resulting table element
6844 constructDataGrid: function(){
6845 // If the data grid has already been built, nothing to do here
6846 if (this.spreadsheet.datagrid) return this.spreadsheet.datagrid;
6848 var s = this.series,
6849 datagrid = this.spreadsheet.loadDataGrid(),
6850 colgroup = ['<colgroup><col />'],
6851 buttonDownload, buttonSelect, t;
6853 // First row : series' labels
6854 var html = ['<table class="flotr-datagrid"><tr class="first-row">'];
6855 html.push('<th> </th>');
6856 _.each(s, function(serie,i){
6857 html.push('<th scope="col">'+(serie.label || String.fromCharCode(65+i))+'</th>');
6858 colgroup.push('<col />');
6862 _.each(datagrid, function(row){
6864 _.times(s.length+1, function(i){
6867 // TODO: do we really want to handle problems with floating point
6869 content = (!_.isUndefined(value) ? Math.round(value*100000)/100000 : '');
6872 var label = getRowLabel.call(this, content);
6873 if (label) content = label;
6876 html.push('<'+tag+(tag=='th'?' scope="row"':'')+'>'+content+'</'+tag+'>');
6880 colgroup.push('</colgroup>');
6881 t = D.node(html.join(''));
6884 * @TODO disabled this
6885 if (!Flotr.isIE || Flotr.isIE == 9) {
6886 function handleMouseout(){
6887 t.select('colgroup col.hover, th.hover').invoke('removeClassName', 'hover');
6889 function handleMouseover(e){
6890 var td = e.element(),
6891 siblings = td.previousSiblings();
6892 t.select('th[scope=col]')[siblings.length-1].addClassName('hover');
6893 t.select('colgroup col')[siblings.length].addClassName('hover');
6895 _.each(t.select('td'), function(td) {
6897 observe(td, 'mouseover', handleMouseover).
6898 observe(td, 'mouseout', handleMouseout);
6903 buttonDownload = D.node(
6904 '<button type="button" class="flotr-datagrid-toolbar-button">' +
6905 this.options.spreadsheet.toolbarDownload +
6908 buttonSelect = D.node(
6909 '<button type="button" class="flotr-datagrid-toolbar-button">' +
6910 this.options.spreadsheet.toolbarSelectAll+
6914 observe(buttonDownload, 'click', _.bind(this.spreadsheet.downloadCSV, this)).
6915 observe(buttonSelect, 'click', _.bind(this.spreadsheet.selectAllData, this));
6917 var toolbar = D.node('<div class="flotr-datagrid-toolbar"></div>');
6918 D.insert(toolbar, buttonDownload);
6919 D.insert(toolbar, buttonSelect);
6921 var containerHeight =this.canvasHeight - D.size(this.spreadsheet.tabsContainer).height-2,
6922 container = D.node('<div class="flotr-datagrid-container" style="position:absolute;left:0px;top:0px;width:'+
6923 this.canvasWidth+'px;height:'+containerHeight+'px;overflow:auto;z-index:10"></div>');
6925 D.insert(container, toolbar);
6926 D.insert(container, t);
6927 D.insert(this.el, container);
6928 this.spreadsheet.datagrid = t;
6929 this.spreadsheet.container = container;
6934 * Shows the specified tab, by its name
6935 * @todo make a tab manager (Flotr.Tabs)
6936 * @param {String} tabName - The tab name
6938 showTab: function(tabName){
6939 if (this.spreadsheet.activeTab === tabName){
6944 D.hide(this.spreadsheet.container);
6945 D.removeClass(this.spreadsheet.tabs.data, 'selected');
6946 D.addClass(this.spreadsheet.tabs.graph, 'selected');
6949 if (!this.spreadsheet.datagrid)
6950 this.spreadsheet.constructDataGrid();
6951 D.show(this.spreadsheet.container);
6952 D.addClass(this.spreadsheet.tabs.data, 'selected');
6953 D.removeClass(this.spreadsheet.tabs.graph, 'selected');
6956 throw 'Illegal tab name: ' + tabName;
6958 this.spreadsheet.activeTab = tabName;
6961 * Selects the data table in the DOM for copy/paste
6963 selectAllData: function(){
6964 if (this.spreadsheet.tabs) {
6965 var selection, range, doc, win, node = this.spreadsheet.constructDataGrid();
6967 this.spreadsheet.showTab('data');
6969 // deferred to be able to select the table
6970 setTimeout(function () {
6971 if ((doc = node.ownerDocument) && (win = doc.defaultView) &&
6972 win.getSelection && doc.createRange &&
6973 (selection = window.getSelection()) &&
6974 selection.removeAllRanges) {
6975 range = doc.createRange();
6976 range.selectNode(node);
6977 selection.removeAllRanges();
6978 selection.addRange(range);
6980 else if (document.body && document.body.createTextRange &&
6981 (range = document.body.createTextRange())) {
6982 range.moveToElementText(node);
6991 * Converts the data into CSV in order to download a file
6993 downloadCSV: function(){
6995 series = this.series,
6996 options = this.options,
6997 dg = this.spreadsheet.loadDataGrid(),
6998 separator = encodeURIComponent(options.spreadsheet.csvFileSeparator);
7000 if (options.spreadsheet.decimalSeparator === options.spreadsheet.csvFileSeparator) {
7001 throw "The decimal separator is the same as the column separator ("+options.spreadsheet.decimalSeparator+")";
7005 _.each(series, function(serie, i){
7006 csv += separator+'"'+(serie.label || String.fromCharCode(65+i)).replace(/\"/g, '\\"')+'"';
7009 csv += "%0D%0A"; // \r\n
7012 csv += _.reduce(dg, function(memo, row){
7013 var rowLabel = getRowLabel.call(this, row[0]) || '';
7014 rowLabel = '"'+(rowLabel+'').replace(/\"/g, '\\"')+'"';
7015 var numbers = row.slice(1).join(separator);
7016 if (options.spreadsheet.decimalSeparator !== '.') {
7017 numbers = numbers.replace(/\./g, options.spreadsheet.decimalSeparator);
7019 return memo + rowLabel+separator+numbers+"%0D%0A"; // \t and \r\n
7022 if (Flotr.isIE && Flotr.isIE < 9) {
7023 csv = csv.replace(new RegExp(separator, 'g'), decodeURIComponent(separator)).replace(/%0A/g, '\n').replace(/%0D/g, '\r');
7024 window.open().document.write(csv);
7026 else window.open('data:text/csv,'+csv);
7035 Flotr.addPlugin('titles', {
7037 'flotr:afterdraw': function() {
7038 this.titles.drawTitles();
7042 * Draws the title and the subtitle
7044 drawTitles : function () {
7046 options = this.options,
7047 margin = options.grid.labelMargin,
7051 if (!options.HtmlText && this.textEnabled) {
7053 size: options.fontSize,
7054 color: options.grid.color,
7059 if (options.subtitle){
7061 ctx, options.subtitle,
7062 this.plotOffset.left + this.plotWidth/2,
7063 this.titleHeight + this.subtitleHeight - 2,
7075 this.plotOffset.left + this.plotWidth/2,
7076 this.titleHeight - 2,
7085 if (a.x.options.title && a.x.used){
7086 style.textAlign = a.x.options.titleAlign || 'center';
7087 style.textBaseline = 'top';
7088 style.angle = Flotr.toRad(a.x.options.titleAngle);
7089 style = Flotr.getBestTextAlign(style.angle, style);
7091 ctx, a.x.options.title,
7092 this.plotOffset.left + this.plotWidth/2,
7093 this.plotOffset.top + a.x.maxLabel.height + this.plotHeight + 2 * margin,
7098 // Add x2 axis title
7099 if (a.x2.options.title && a.x2.used){
7100 style.textAlign = a.x2.options.titleAlign || 'center';
7101 style.textBaseline = 'bottom';
7102 style.angle = Flotr.toRad(a.x2.options.titleAngle);
7103 style = Flotr.getBestTextAlign(style.angle, style);
7105 ctx, a.x2.options.title,
7106 this.plotOffset.left + this.plotWidth/2,
7107 this.plotOffset.top - a.x2.maxLabel.height - 2 * margin,
7113 if (a.y.options.title && a.y.used){
7114 style.textAlign = a.y.options.titleAlign || 'right';
7115 style.textBaseline = 'middle';
7116 style.angle = Flotr.toRad(a.y.options.titleAngle);
7117 style = Flotr.getBestTextAlign(style.angle, style);
7119 ctx, a.y.options.title,
7120 this.plotOffset.left - a.y.maxLabel.width - 2 * margin,
7121 this.plotOffset.top + this.plotHeight / 2,
7126 // Add y2 axis title
7127 if (a.y2.options.title && a.y2.used){
7128 style.textAlign = a.y2.options.titleAlign || 'left';
7129 style.textBaseline = 'middle';
7130 style.angle = Flotr.toRad(a.y2.options.titleAngle);
7131 style = Flotr.getBestTextAlign(style.angle, style);
7133 ctx, a.y2.options.title,
7134 this.plotOffset.left + this.plotWidth + a.y2.maxLabel.width + 2 * margin,
7135 this.plotOffset.top + this.plotHeight / 2,
7146 '<div style="position:absolute;top:0;left:',
7147 this.plotOffset.left, 'px;font-size:1em;font-weight:bold;text-align:center;width:',
7148 this.plotWidth,'px;" class="flotr-title">', options.title, '</div>'
7152 if (options.subtitle)
7154 '<div style="position:absolute;top:', this.titleHeight, 'px;left:',
7155 this.plotOffset.left, 'px;font-size:smaller;text-align:center;width:',
7156 this.plotWidth, 'px;" class="flotr-subtitle">', options.subtitle, '</div>'
7159 html.push('</div>');
7161 html.push('<div class="flotr-axis-title" style="font-weight:bold;">');
7164 if (a.x.options.title && a.x.used)
7166 '<div style="position:absolute;top:',
7167 (this.plotOffset.top + this.plotHeight + options.grid.labelMargin + a.x.titleSize.height),
7168 'px;left:', this.plotOffset.left, 'px;width:', this.plotWidth,
7169 'px;text-align:', a.x.options.titleAlign, ';" class="flotr-axis-title flotr-axis-title-x1">', a.x.options.title, '</div>'
7172 // Add x2 axis title
7173 if (a.x2.options.title && a.x2.used)
7175 '<div style="position:absolute;top:0;left:', this.plotOffset.left, 'px;width:',
7176 this.plotWidth, 'px;text-align:', a.x2.options.titleAlign, ';" class="flotr-axis-title flotr-axis-title-x2">', a.x2.options.title, '</div>'
7180 if (a.y.options.title && a.y.used)
7182 '<div style="position:absolute;top:',
7183 (this.plotOffset.top + this.plotHeight/2 - a.y.titleSize.height/2),
7184 'px;left:0;text-align:', a.y.options.titleAlign, ';" class="flotr-axis-title flotr-axis-title-y1">', a.y.options.title, '</div>'
7187 // Add y2 axis title
7188 if (a.y2.options.title && a.y2.used)
7190 '<div style="position:absolute;top:',
7191 (this.plotOffset.top + this.plotHeight/2 - a.y.titleSize.height/2),
7192 'px;right:0;text-align:', a.y2.options.titleAlign, ';" class="flotr-axis-title flotr-axis-title-y2">', a.y2.options.title, '</div>'
7195 html = html.join('');
7197 var div = D.create('div');
7199 color: options.grid.color
7201 div.className = 'flotr-titles';
7202 D.insert(this.el, div);
7203 D.insert(div, html);