From cf509bdb640363a15f885d072acfd6254e5a9fd2 Mon Sep 17 00:00:00 2001 From: Ian Hopkinson Date: Tue, 25 Jun 2024 11:42:54 +0100 Subject: [PATCH 01/17] HDX-9931 Install psycopg2-binary to fix pip-compile issue --- .gitignore | 2 ++ requirements.in | 2 +- requirements.txt | 45 +++++++++++++++++++++++---------------------- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index 0965f533c9..aeaba08830 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,5 @@ travis.env # cypress cypress/videos + +src/ diff --git a/requirements.in b/requirements.in index 263d0c41bd..8b134856e7 100644 --- a/requirements.in +++ b/requirements.in @@ -20,7 +20,7 @@ PyJWT==2.4.0 Markdown==3.4.1 passlib==1.7.4 polib==1.1.1 -psycopg2==2.9.3 +psycopg2-binary==2.9.3 python-magic==0.4.27 pysolr==3.9.0 python-dateutil==2.8.2 diff --git a/requirements.txt b/requirements.txt index 56bd710131..449dbc6ca1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,6 +16,8 @@ # via -r requirements.in alembic==1.8.1 # via -r requirements.in +async-timeout==4.0.3 + # via redis babel==2.10.3 # via # -r requirements.in @@ -28,11 +30,11 @@ blinker==1.5 # via -r requirements.in boto3==1.34.77 # via ckanext-s3filestore -botocore==1.34.77 +botocore==1.34.132 # via # boto3 # s3transfer -certifi==2021.5.30 +certifi==2024.6.2 # via requests charset-normalizer==2.1.1 # via requests @@ -67,13 +69,13 @@ flask-multistatic==1.0 # via -r requirements.in flask-wtf==1.0.1 # via -r requirements.in -geomet==1.0.0 +geomet==1.1.0 # via -r requirements.in greenlet==2.0.2 # via # -r requirements.in # sqlalchemy -idna==2.8 +idna==3.7 # via requests ijson==3.2.3 # via -r requirements.in @@ -81,7 +83,7 @@ ipaddress==1.0.23 # via -r requirements.in isodate==0.6.1 # via rdflib -itsdangerous==2.1.2 +itsdangerous==2.2.0 # via # flask # flask-wtf @@ -90,24 +92,24 @@ jinja2==3.1.2 # -r requirements.in # flask # flask-babel -jmespath==0.10.0 +jmespath==1.0.1 # via # boto3 # botocore -lxml==4.9.1 +lxml==5.2.2 # via feedgen mailchimp3==3.0.21 # via -r requirements.in -mako==1.1.5 +mako==1.3.5 # via alembic markdown==3.4.1 # via -r requirements.in -markupsafe==2.1.3 +markupsafe==2.1.5 # via # jinja2 # mako # wtforms -mypy==1.7.0 +mypy==1.10.1 # via sqlalchemy mypy-extensions==1.0.0 # via mypy @@ -119,13 +121,13 @@ pbr==6.0.0 # via stevedore polib==1.1.1 # via -r requirements.in -psycopg2==2.9.3 +psycopg2-binary==2.9.3 # via -r requirements.in pyjwt==2.4.0 # via -r requirements.in pyotp==2.6.0 # via -r requirements.in -pyparsing==3.0.7 +pyparsing==3.1.2 # via rdflib pysolr==3.9.0 # via -r requirements.in @@ -138,7 +140,7 @@ python-json-logger==2.0.7 # via -r requirements.in python-magic==0.4.27 # via -r requirements.in -pytz==2023.3 +pytz==2024.1 # via # -r requirements.in # babel @@ -155,7 +157,7 @@ rdflib==4.2.1 # rdflib-jsonld rdflib-jsonld==0.4.0 # via -r requirements.in -redis==3.5.3 +redis==5.0.6 # via rq requests==2.28.1 # via @@ -164,14 +166,13 @@ requests==2.28.1 # pysolr rq==1.11.0 # via -r requirements.in -s3transfer==0.10.1 +s3transfer==0.10.2 # via boto3 simplejson==3.17.6 # via -r requirements.in six==1.16.0 # via # bleach - # geomet # isodate # python-dateutil # pyutilib @@ -180,11 +181,11 @@ sqlalchemy[mypy]==1.4.41 # -r requirements.in # alembic # sqlalchemy -sqlalchemy2-stubs==0.0.2a37 +sqlalchemy2-stubs==0.0.2a38 # via sqlalchemy sqlparse==0.4.2 # via -r requirements.in -stevedore==5.1.0 +stevedore==5.2.0 # via dogpile-cache timeago==1.0.16 # via -r requirements.in @@ -196,7 +197,7 @@ typing-extensions==4.3.0 # dogpile-cache # mypy # sqlalchemy2-stubs -tzdata==2023.3 +tzdata==2024.1 # via pytz-deprecation-shim tzlocal==4.2 # via -r requirements.in @@ -204,13 +205,13 @@ ua-parser==0.18.0 # via # -r requirements.in # user-agents -urllib3==1.26.6 +urllib3==1.26.19 # via # botocore # requests user-agents==2.2.0 # via -r requirements.in -watchdog==2.1.5 +watchdog==4.0.1 # via werkzeug webassets==2.0 # via -r requirements.in @@ -222,7 +223,7 @@ werkzeug[watchdog]==2.0.3 # flask # flask-login # werkzeug -wtforms==3.1.1 +wtforms==3.1.2 # via flask-wtf zope-interface==5.4.0 # via -r requirements.in From 1e7815ecbdbf0696745ad8d578390d69500934ab Mon Sep 17 00:00:00 2001 From: Ian Hopkinson Date: Thu, 27 Jun 2024 13:20:05 +0100 Subject: [PATCH 02/17] HDX-9931 - progress to running locally --- .devcontainer/README.md | 2 + .gitattributes | 3 + .vscode/settings.json | 7 ++ ckanext-hdx_theme/ckanext/hdx_theme/less | 1 - docker/hdxckantool-ng.py | 90 +++++++++++++++++++----- 5 files changed, 85 insertions(+), 18 deletions(-) create mode 100644 .vscode/settings.json delete mode 120000 ckanext-hdx_theme/ckanext/hdx_theme/less diff --git a/.devcontainer/README.md b/.devcontainer/README.md index d0d24540da..26d48dc6a7 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -5,6 +5,8 @@ 1. Add the "Dev Containers" extension 1. Clone this git repository 1. Open this repo in VSCode +1. Remember to create an `.env.secrets` file in the `.devcontainer` folder from `.env.secrets.template` +1. Run `docker network create proxy`? 1. Click the popup saying "Reopen in container" 1. Wait for a few minutes while everything is set up for the first time diff --git a/.gitattributes b/.gitattributes index c2fb8804b9..bea05ec04a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,5 @@ #*.po -diff *.mo -diff + +* text=auto eol=lf + diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..918b352428 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "ckan/tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/ckanext-hdx_theme/ckanext/hdx_theme/less b/ckanext-hdx_theme/ckanext/hdx_theme/less deleted file mode 120000 index d39c2bfe0c..0000000000 --- a/ckanext-hdx_theme/ckanext/hdx_theme/less +++ /dev/null @@ -1 +0,0 @@ -hdx-styles/src/common/less \ No newline at end of file diff --git a/docker/hdxckantool-ng.py b/docker/hdxckantool-ng.py index 8127a10bc5..0e7b976eb6 100755 --- a/docker/hdxckantool-ng.py +++ b/docker/hdxckantool-ng.py @@ -48,28 +48,28 @@ def db_connect_to_postgres(host=SQL['HOST'], port=SQL['PORT'], dbname=SQL['DB'], except: raise click.ClickException("I am unable to connect to the database, exiting.") -def db_schema_owner(dbname, schema='public', owner=SQL['USER'], verbose=False): +def db_schema_owner(dbname, schema='public', owner=SQL['USER'], verbose=True): """assign a new owner to a schema.""" - try: - if verbose: - print("Assigning {} as owner of {} schema of {} database...".format(owner, schema, dbname)) - con = db_connect_to_postgres(dbname=dbname, user=SQL['SUPERUSER'], password=SQL['SUPERPASS']) - con.set_isolation_level(0) - cur = con.cursor() - query = "alter schema {} owner to {};".format(schema, owner) - cur.execute(query) - con.commit() - if verbose: - print("Done.") - except: - raise click.ClickException("I can't assign a new owner") - finally: - con.close() + # try: + if verbose: + print("Assigning {} as owner of {} schema of {} database...".format(owner, schema, dbname)) + con = db_connect_to_postgres(dbname=dbname, user=SQL['SUPERUSER'], password=SQL['SUPERPASS']) + con.set_isolation_level(0) + cur = con.cursor() + query = "alter schema {} owner to {};".format(schema, owner) + cur.execute(query) + con.commit() + if verbose: + print("Done.") + # except: + # raise click.ClickException("I can't assign a new owner") + # finally: + # con.close() -def db_empty(dbname, verbose=False): +def db_empty(dbname, verbose=True): """Recreate the schema for a database.""" try: @@ -892,6 +892,62 @@ def do_magic(ctx, force, solr_config): # solr reindex time ctx.invoke(solr_reindex, clear=True, fast=True) +@cli.command(name='test') +@click.option('-f', '--force', is_flag=True, default=False, show_default=True, help="Just go.") +@click.option('-s', '--solr-config', default=SOLR['CONFIGSET'], show_default=True, help="SOLR configset name") +@click.pass_context +def do_test(ctx, force, solr_config): + """Does sufficient setup for tests to run""" + + print('Will do all the initial setup for you. Or just refresh the dbs and files for you') + print(' - pulls the latest db backup') + print(' - restores the database') + print(' - pulls the latest files backup (other than the filestore)') + print(' - restores the files (other than filestore)') + print(' - syncronizes the filestore from the dev filestore') + print(' - creates a new solr collection') + print(' - reindex solr') + print('Please be warned this will take quite a while') + print(' (especially the filestore sync and the solr reindex)') + + if not force: + whee = input('Do you want to proceed? [y/n]: ') + if whee not in ['y', 'Y']: + print('Maybe later then.') + return + + # print('You will be asked to enter your username and password used to get the database snapshots.') + # # pull latest dbs + # ctx.invoke(db_pull, all=True) + # # pull latest files + # ctx.invoke(files_pull) + + # add pgpass + ctx.invoke(refresh_pgpass_command) + # fix schemas + ctx.invoke(db_set_schema) + # create solr collection + ctx.invoke(solr_add, force=True, config_set=solr_config) # fix or parametrize this. innovation has 'hdx-solr-main' + + # restore dbs and files + # ctx.invoke(db_restore, database='datastore', filename='/srv/backup/datastore.pg_restore', clear_database=True) + # ctx.invoke(db_restore, database='ckan', filename='/srv/backup/ckan.pg_restore', minimal=True, clear_database=True) + # # fix datastore permissions + ctx.invoke(db_set_perms) + + # restore files + # ctx.invoke(files_restore, targetdir='/srv/filestore', filename='/srv/backup/files.tar') + # # sync filestore + # ctx.invoke(filestore_sync, + # source='hdx-dev-filestore', + # destination=os.getenv('AWS_BUCKET_NAME'), + # source_region='us-east-1', + # region=os.getenv('REGION_NAME'), + # clear=True) + + # solr reindex time + # ctx.invoke(solr_reindex, clear=True, fast=True) + if __name__ == '__main__': cli() From 783bfffcbf504d91c03b491485e96d319ac02b1e Mon Sep 17 00:00:00 2001 From: Ian Hopkinson Date: Fri, 28 Jun 2024 09:06:20 +0100 Subject: [PATCH 03/17] HDX-9931 Apply --renormalize to try to fix line endings --- .gitattributes | 1 + .../theme/public/vendor/flotr2/flotr2.js | 12256 ++++++++-------- .../public/vendor/leaflet/0.7.7/leaflet.css | 958 +- .../public/vendor/slickgrid/2.2/slick.grid.js | 6844 ++++----- 4 files changed, 10030 insertions(+), 10029 deletions(-) diff --git a/.gitattributes b/.gitattributes index bea05ec04a..ac5417e47a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,4 +2,5 @@ *.mo -diff * text=auto eol=lf +*.sh eol=lf diff --git a/ckanext/reclineview/theme/public/vendor/flotr2/flotr2.js b/ckanext/reclineview/theme/public/vendor/flotr2/flotr2.js index 13447f9cdd..15c4c8fc83 100644 --- a/ckanext/reclineview/theme/public/vendor/flotr2/flotr2.js +++ b/ckanext/reclineview/theme/public/vendor/flotr2/flotr2.js @@ -1,6128 +1,6128 @@ -/*! - * bean.js - copyright Jacob Thornton 2011 - * https://github.com/fat/bean - * MIT License - * special thanks to: - * dean edwards: http://dean.edwards.name/ - * dperini: https://github.com/dperini/nwevents - * the entire mootools team: github.com/mootools/mootools-core - */ -/*global module:true, define:true*/ -!function (name, context, definition) { - if (typeof module !== 'undefined') module.exports = definition(name, context); - else if (typeof define === 'function' && typeof define.amd === 'object') define(definition); - else context[name] = definition(name, context); -}('bean', this, function (name, context) { - var win = window - , old = context[name] - , overOut = /over|out/ - , namespaceRegex = /[^\.]*(?=\..*)\.|.*/ - , nameRegex = /\..*/ - , addEvent = 'addEventListener' - , attachEvent = 'attachEvent' - , removeEvent = 'removeEventListener' - , detachEvent = 'detachEvent' - , doc = document || {} - , root = doc.documentElement || {} - , W3C_MODEL = root[addEvent] - , eventSupport = W3C_MODEL ? addEvent : attachEvent - , slice = Array.prototype.slice - , mouseTypeRegex = /click|mouse|menu|drag|drop/i - , touchTypeRegex = /^touch|^gesture/i - , ONE = { one: 1 } // singleton for quick matching making add() do one() - - , nativeEvents = (function (hash, events, i) { - for (i = 0; i < events.length; i++) - hash[events[i]] = 1 - return hash - })({}, ( - 'click dblclick mouseup mousedown contextmenu ' + // mouse buttons - 'mousewheel DOMMouseScroll ' + // mouse wheel - 'mouseover mouseout mousemove selectstart selectend ' + // mouse movement - 'keydown keypress keyup ' + // keyboard - 'orientationchange ' + // mobile - 'focus blur change reset select submit ' + // form elements - 'load unload beforeunload resize move DOMContentLoaded readystatechange ' + // window - 'error abort scroll ' + // misc - (W3C_MODEL ? // element.fireEvent('onXYZ'... is not forgiving if we try to fire an event - // that doesn't actually exist, so make sure we only do these on newer browsers - 'show ' + // mouse buttons - 'input invalid ' + // form elements - 'touchstart touchmove touchend touchcancel ' + // touch - 'gesturestart gesturechange gestureend ' + // gesture - 'message readystatechange pageshow pagehide popstate ' + // window - 'hashchange offline online ' + // window - 'afterprint beforeprint ' + // printing - 'dragstart dragenter dragover dragleave drag drop dragend ' + // dnd - 'loadstart progress suspend emptied stalled loadmetadata ' + // media - 'loadeddata canplay canplaythrough playing waiting seeking ' + // media - 'seeked ended durationchange timeupdate play pause ratechange ' + // media - 'volumechange cuechange ' + // media - 'checking noupdate downloading cached updateready obsolete ' + // appcache - '' : '') - ).split(' ') - ) - - , customEvents = (function () { - function isDescendant(parent, node) { - while ((node = node.parentNode) !== null) { - if (node === parent) return true - } - return false - } - - function check(event) { - var related = event.relatedTarget - if (!related) return related === null - return (related !== this && related.prefix !== 'xul' && !/document/.test(this.toString()) && !isDescendant(this, related)) - } - - return { - mouseenter: { base: 'mouseover', condition: check } - , mouseleave: { base: 'mouseout', condition: check } - , mousewheel: { base: /Firefox/.test(navigator.userAgent) ? 'DOMMouseScroll' : 'mousewheel' } - } - })() - - , fixEvent = (function () { - var commonProps = 'altKey attrChange attrName bubbles cancelable ctrlKey currentTarget detail eventPhase getModifierState isTrusted metaKey relatedNode relatedTarget shiftKey srcElement target timeStamp type view which'.split(' ') - , mouseProps = commonProps.concat('button buttons clientX clientY dataTransfer fromElement offsetX offsetY pageX pageY screenX screenY toElement'.split(' ')) - , keyProps = commonProps.concat('char charCode key keyCode'.split(' ')) - , touchProps = commonProps.concat('touches targetTouches changedTouches scale rotation'.split(' ')) - , preventDefault = 'preventDefault' - , createPreventDefault = function (event) { - return function () { - if (event[preventDefault]) - event[preventDefault]() - else - event.returnValue = false - } - } - , stopPropagation = 'stopPropagation' - , createStopPropagation = function (event) { - return function () { - if (event[stopPropagation]) - event[stopPropagation]() - else - event.cancelBubble = true - } - } - , createStop = function (synEvent) { - return function () { - synEvent[preventDefault]() - synEvent[stopPropagation]() - synEvent.stopped = true - } - } - , copyProps = function (event, result, props) { - var i, p - for (i = props.length; i--;) { - p = props[i] - if (!(p in result) && p in event) result[p] = event[p] - } - } - - return function (event, isNative) { - var result = { originalEvent: event, isNative: isNative } - if (!event) - return result - - var props - , type = event.type - , target = event.target || event.srcElement - - result[preventDefault] = createPreventDefault(event) - result[stopPropagation] = createStopPropagation(event) - result.stop = createStop(result) - result.target = target && target.nodeType === 3 ? target.parentNode : target - - if (isNative) { // we only need basic augmentation on custom events, the rest is too expensive - if (type.indexOf('key') !== -1) { - props = keyProps - result.keyCode = event.which || event.keyCode - } else if (mouseTypeRegex.test(type)) { - props = mouseProps - result.rightClick = event.which === 3 || event.button === 2 - result.pos = { x: 0, y: 0 } - if (event.pageX || event.pageY) { - result.clientX = event.pageX - result.clientY = event.pageY - } else if (event.clientX || event.clientY) { - result.clientX = event.clientX + doc.body.scrollLeft + root.scrollLeft - result.clientY = event.clientY + doc.body.scrollTop + root.scrollTop - } - if (overOut.test(type)) - result.relatedTarget = event.relatedTarget || event[(type === 'mouseover' ? 'from' : 'to') + 'Element'] - } else if (touchTypeRegex.test(type)) { - props = touchProps - } - copyProps(event, result, props || commonProps) - } - return result - } - })() - - // if we're in old IE we can't do onpropertychange on doc or win so we use doc.documentElement for both - , targetElement = function (element, isNative) { - return !W3C_MODEL && !isNative && (element === doc || element === win) ? root : element - } - - // we use one of these per listener, of any type - , RegEntry = (function () { - function entry(element, type, handler, original, namespaces) { - this.element = element - this.type = type - this.handler = handler - this.original = original - this.namespaces = namespaces - this.custom = customEvents[type] - this.isNative = nativeEvents[type] && element[eventSupport] - this.eventType = W3C_MODEL || this.isNative ? type : 'propertychange' - this.customType = !W3C_MODEL && !this.isNative && type - this.target = targetElement(element, this.isNative) - this.eventSupport = this.target[eventSupport] - } - - entry.prototype = { - // given a list of namespaces, is our entry in any of them? - inNamespaces: function (checkNamespaces) { - var i, j - if (!checkNamespaces) - return true - if (!this.namespaces) - return false - for (i = checkNamespaces.length; i--;) { - for (j = this.namespaces.length; j--;) { - if (checkNamespaces[i] === this.namespaces[j]) - return true - } - } - return false - } - - // match by element, original fn (opt), handler fn (opt) - , matches: function (checkElement, checkOriginal, checkHandler) { - return this.element === checkElement && - (!checkOriginal || this.original === checkOriginal) && - (!checkHandler || this.handler === checkHandler) - } - } - - return entry - })() - - , registry = (function () { - // our map stores arrays by event type, just because it's better than storing - // everything in a single array. uses '$' as a prefix for the keys for safety - var map = {} - - // generic functional search of our registry for matching listeners, - // `fn` returns false to break out of the loop - , forAll = function (element, type, original, handler, fn) { - if (!type || type === '*') { - // search the whole registry - for (var t in map) { - if (t.charAt(0) === '$') - forAll(element, t.substr(1), original, handler, fn) - } - } else { - var i = 0, l, list = map['$' + type], all = element === '*' - if (!list) - return - for (l = list.length; i < l; i++) { - if (all || list[i].matches(element, original, handler)) - if (!fn(list[i], list, i, type)) - return - } - } - } - - , has = function (element, type, original) { - // we're not using forAll here simply because it's a bit slower and this - // needs to be fast - var i, list = map['$' + type] - if (list) { - for (i = list.length; i--;) { - if (list[i].matches(element, original, null)) - return true - } - } - return false - } - - , get = function (element, type, original) { - var entries = [] - forAll(element, type, original, null, function (entry) { return entries.push(entry) }) - return entries - } - - , put = function (entry) { - (map['$' + entry.type] || (map['$' + entry.type] = [])).push(entry) - return entry - } - - , del = function (entry) { - forAll(entry.element, entry.type, null, entry.handler, function (entry, list, i) { - list.splice(i, 1) - if (list.length === 0) - delete map['$' + entry.type] - return false - }) - } - - // dump all entries, used for onunload - , entries = function () { - var t, entries = [] - for (t in map) { - if (t.charAt(0) === '$') - entries = entries.concat(map[t]) - } - return entries - } - - return { has: has, get: get, put: put, del: del, entries: entries } - })() - - // add and remove listeners to DOM elements - , listener = W3C_MODEL ? function (element, type, fn, add) { - element[add ? addEvent : removeEvent](type, fn, false) - } : function (element, type, fn, add, custom) { - if (custom && add && element['_on' + custom] === null) - element['_on' + custom] = 0 - element[add ? attachEvent : detachEvent]('on' + type, fn) - } - - , nativeHandler = function (element, fn, args) { - return function (event) { - event = fixEvent(event || ((this.ownerDocument || this.document || this).parentWindow || win).event, true) - return fn.apply(element, [event].concat(args)) - } - } - - , customHandler = function (element, fn, type, condition, args, isNative) { - return function (event) { - if (condition ? condition.apply(this, arguments) : W3C_MODEL ? true : event && event.propertyName === '_on' + type || !event) { - if (event) - event = fixEvent(event || ((this.ownerDocument || this.document || this).parentWindow || win).event, isNative) - fn.apply(element, event && (!args || args.length === 0) ? arguments : slice.call(arguments, event ? 0 : 1).concat(args)) - } - } - } - - , once = function (rm, element, type, fn, originalFn) { - // wrap the handler in a handler that does a remove as well - return function () { - rm(element, type, originalFn) - fn.apply(this, arguments) - } - } - - , removeListener = function (element, orgType, handler, namespaces) { - var i, l, entry - , type = (orgType && orgType.replace(nameRegex, '')) - , handlers = registry.get(element, type, handler) - - for (i = 0, l = handlers.length; i < l; i++) { - if (handlers[i].inNamespaces(namespaces)) { - if ((entry = handlers[i]).eventSupport) - listener(entry.target, entry.eventType, entry.handler, false, entry.type) - // TODO: this is problematic, we have a registry.get() and registry.del() that - // both do registry searches so we waste cycles doing this. Needs to be rolled into - // a single registry.forAll(fn) that removes while finding, but the catch is that - // we'll be splicing the arrays that we're iterating over. Needs extra tests to - // make sure we don't screw it up. @rvagg - registry.del(entry) - } - } - } - - , addListener = function (element, orgType, fn, originalFn, args) { - var entry - , type = orgType.replace(nameRegex, '') - , namespaces = orgType.replace(namespaceRegex, '').split('.') - - if (registry.has(element, type, fn)) - return element // no dupe - if (type === 'unload') - fn = once(removeListener, element, type, fn, originalFn) // self clean-up - if (customEvents[type]) { - if (customEvents[type].condition) - fn = customHandler(element, fn, type, customEvents[type].condition, true) - type = customEvents[type].base || type - } - entry = registry.put(new RegEntry(element, type, fn, originalFn, namespaces[0] && namespaces)) - entry.handler = entry.isNative ? - nativeHandler(element, entry.handler, args) : - customHandler(element, entry.handler, type, false, args, false) - if (entry.eventSupport) - listener(entry.target, entry.eventType, entry.handler, true, entry.customType) - } - - , del = function (selector, fn, $) { - return function (e) { - var target, i, array = typeof selector === 'string' ? $(selector, this) : selector - for (target = e.target; target && target !== this; target = target.parentNode) { - for (i = array.length; i--;) { - if (array[i] === target) { - return fn.apply(target, arguments) - } - } - } - } - } - - , remove = function (element, typeSpec, fn) { - var k, m, type, namespaces, i - , rm = removeListener - , isString = typeSpec && typeof typeSpec === 'string' - - if (isString && typeSpec.indexOf(' ') > 0) { - // remove(el, 't1 t2 t3', fn) or remove(el, 't1 t2 t3') - typeSpec = typeSpec.split(' ') - for (i = typeSpec.length; i--;) - remove(element, typeSpec[i], fn) - return element - } - type = isString && typeSpec.replace(nameRegex, '') - if (type && customEvents[type]) - type = customEvents[type].type - if (!typeSpec || isString) { - // remove(el) or remove(el, t1.ns) or remove(el, .ns) or remove(el, .ns1.ns2.ns3) - if (namespaces = isString && typeSpec.replace(namespaceRegex, '')) - namespaces = namespaces.split('.') - rm(element, type, fn, namespaces) - } else if (typeof typeSpec === 'function') { - // remove(el, fn) - rm(element, null, typeSpec) - } else { - // remove(el, { t1: fn1, t2, fn2 }) - for (k in typeSpec) { - if (typeSpec.hasOwnProperty(k)) - remove(element, k, typeSpec[k]) - } - } - return element - } - - , add = function (element, events, fn, delfn, $) { - var type, types, i, args - , originalFn = fn - , isDel = fn && typeof fn === 'string' - - if (events && !fn && typeof events === 'object') { - for (type in events) { - if (events.hasOwnProperty(type)) - add.apply(this, [ element, type, events[type] ]) - } - } else { - args = arguments.length > 3 ? slice.call(arguments, 3) : [] - types = (isDel ? fn : events).split(' ') - isDel && (fn = del(events, (originalFn = delfn), $)) && (args = slice.call(args, 1)) - // special case for one() - this === ONE && (fn = once(remove, element, events, fn, originalFn)) - for (i = types.length; i--;) addListener(element, types[i], fn, originalFn, args) - } - return element - } - - , one = function () { - return add.apply(ONE, arguments) - } - - , fireListener = W3C_MODEL ? function (isNative, type, element) { - var evt = doc.createEvent(isNative ? 'HTMLEvents' : 'UIEvents') - evt[isNative ? 'initEvent' : 'initUIEvent'](type, true, true, win, 1) - element.dispatchEvent(evt) - } : function (isNative, type, element) { - element = targetElement(element, isNative) - // if not-native then we're using onpropertychange so we just increment a custom property - isNative ? element.fireEvent('on' + type, doc.createEventObject()) : element['_on' + type]++ - } - - , fire = function (element, type, args) { - var i, j, l, names, handlers - , types = type.split(' ') - - for (i = types.length; i--;) { - type = types[i].replace(nameRegex, '') - if (names = types[i].replace(namespaceRegex, '')) - names = names.split('.') - if (!names && !args && element[eventSupport]) { - fireListener(nativeEvents[type], type, element) - } else { - // non-native event, either because of a namespace, arguments or a non DOM element - // iterate over all listeners and manually 'fire' - handlers = registry.get(element, type) - args = [false].concat(args) - for (j = 0, l = handlers.length; j < l; j++) { - if (handlers[j].inNamespaces(names)) - handlers[j].handler.apply(element, args) - } - } - } - return element - } - - , clone = function (element, from, type) { - var i = 0 - , handlers = registry.get(from, type) - , l = handlers.length - - for (;i < l; i++) - handlers[i].original && add(element, handlers[i].type, handlers[i].original) - return element - } - - , bean = { - add: add - , one: one - , remove: remove - , clone: clone - , fire: fire - , noConflict: function () { - context[name] = old - return this - } - } - - if (win[attachEvent]) { - // for IE, clean up on unload to avoid leaks - var cleanup = function () { - var i, entries = registry.entries() - for (i in entries) { - if (entries[i].type && entries[i].type !== 'unload') - remove(entries[i].element, entries[i].type) - } - win[detachEvent]('onunload', cleanup) - win.CollectGarbage && win.CollectGarbage() - } - win[attachEvent]('onunload', cleanup) - } - - return bean -}); - -/** - * Flotr2 (c) 2012 Carl Sutherland - * MIT License - * Special thanks to: - * Flotr: http://code.google.com/p/flotr/ (fork) - * Flot: https://github.com/flot/flot (original fork) - */ -(function () { - -var - global = this, - previousFlotr = this.Flotr, - Flotr; - -Flotr = { - _: _, - bean: bean, - isIphone: /iphone/i.test(navigator.userAgent), - isIE: (navigator.appVersion.indexOf("MSIE") != -1 ? parseFloat(navigator.appVersion.split("MSIE")[1]) : false), - - /** - * An object of the registered graph types. Use Flotr.addType(type, object) - * to add your own type. - */ - graphTypes: {}, - - /** - * The list of the registered plugins - */ - plugins: {}, - - /** - * Can be used to add your own chart type. - * @param {String} name - Type of chart, like 'pies', 'bars' etc. - * @param {String} graphType - The object containing the basic drawing functions (draw, etc) - */ - addType: function(name, graphType){ - Flotr.graphTypes[name] = graphType; - Flotr.defaultOptions[name] = graphType.options || {}; - Flotr.defaultOptions.defaultType = Flotr.defaultOptions.defaultType || name; - }, - - /** - * Can be used to add a plugin - * @param {String} name - The name of the plugin - * @param {String} plugin - The object containing the plugin's data (callbacks, options, function1, function2, ...) - */ - addPlugin: function(name, plugin){ - Flotr.plugins[name] = plugin; - Flotr.defaultOptions[name] = plugin.options || {}; - }, - - /** - * Draws the graph. This function is here for backwards compatibility with Flotr version 0.1.0alpha. - * You could also draw graphs by directly calling Flotr.Graph(element, data, options). - * @param {Element} el - element to insert the graph into - * @param {Object} data - an array or object of dataseries - * @param {Object} options - an object containing options - * @param {Class} _GraphKlass_ - (optional) Class to pass the arguments to, defaults to Flotr.Graph - * @return {Object} returns a new graph object and of course draws the graph. - */ - draw: function(el, data, options, GraphKlass){ - GraphKlass = GraphKlass || Flotr.Graph; - return new GraphKlass(el, data, options); - }, - - /** - * Recursively merges two objects. - * @param {Object} src - source object (likely the object with the least properties) - * @param {Object} dest - destination object (optional, object with the most properties) - * @return {Object} recursively merged Object - * @TODO See if we can't remove this. - */ - merge: function(src, dest){ - var i, v, result = dest || {}; - - for (i in src) { - v = src[i]; - if (v && typeof(v) === 'object') { - if (v.constructor === Array) { - result[i] = this._.clone(v); - } else if (v.constructor !== RegExp && !this._.isElement(v)) { - result[i] = Flotr.merge(v, (dest ? dest[i] : undefined)); - } else { - result[i] = v; - } - } else { - result[i] = v; - } - } - - return result; - }, - - /** - * Recursively clones an object. - * @param {Object} object - The object to clone - * @return {Object} the clone - * @TODO See if we can't remove this. - */ - clone: function(object){ - return Flotr.merge(object, {}); - }, - - /** - * Function calculates the ticksize and returns it. - * @param {Integer} noTicks - number of ticks - * @param {Integer} min - lower bound integer value for the current axis - * @param {Integer} max - upper bound integer value for the current axis - * @param {Integer} decimals - number of decimals for the ticks - * @return {Integer} returns the ticksize in pixels - */ - getTickSize: function(noTicks, min, max, decimals){ - var delta = (max - min) / noTicks, - magn = Flotr.getMagnitude(delta), - tickSize = 10, - norm = delta / magn; // Norm is between 1.0 and 10.0. - - if(norm < 1.5) tickSize = 1; - else if(norm < 2.25) tickSize = 2; - else if(norm < 3) tickSize = ((decimals === 0) ? 2 : 2.5); - else if(norm < 7.5) tickSize = 5; - - return tickSize * magn; - }, - - /** - * Default tick formatter. - * @param {String, Integer} val - tick value integer - * @param {Object} axisOpts - the axis' options - * @return {String} formatted tick string - */ - defaultTickFormatter: function(val, axisOpts){ - return val+''; - }, - - /** - * Formats the mouse tracker values. - * @param {Object} obj - Track value Object {x:..,y:..} - * @return {String} Formatted track string - */ - defaultTrackFormatter: function(obj){ - return '('+obj.x+', '+obj.y+')'; - }, - - /** - * Utility function to convert file size values in bytes to kB, MB, ... - * @param value {Number} - The value to convert - * @param precision {Number} - The number of digits after the comma (default: 2) - * @param base {Number} - The base (default: 1000) - */ - engineeringNotation: function(value, precision, base){ - var sizes = ['Y','Z','E','P','T','G','M','k',''], - fractionSizes = ['y','z','a','f','p','n','µ','m',''], - total = sizes.length; - - base = base || 1000; - precision = Math.pow(10, precision || 2); - - if (value === 0) return 0; - - if (value > 1) { - while (total-- && (value >= base)) value /= base; - } - else { - sizes = fractionSizes; - total = sizes.length; - while (total-- && (value < 1)) value *= base; - } - - return (Math.round(value * precision) / precision) + sizes[total]; - }, - - /** - * Returns the magnitude of the input value. - * @param {Integer, Float} x - integer or float value - * @return {Integer, Float} returns the magnitude of the input value - */ - getMagnitude: function(x){ - return Math.pow(10, Math.floor(Math.log(x) / Math.LN10)); - }, - toPixel: function(val){ - return Math.floor(val)+0.5;//((val-Math.round(val) < 0.4) ? (Math.floor(val)-0.5) : val); - }, - toRad: function(angle){ - return -angle * (Math.PI/180); - }, - floorInBase: function(n, base) { - return base * Math.floor(n / base); - }, - drawText: function(ctx, text, x, y, style) { - if (!ctx.fillText) { - ctx.drawText(text, x, y, style); - return; - } - - style = this._.extend({ - size: Flotr.defaultOptions.fontSize, - color: '#000000', - textAlign: 'left', - textBaseline: 'bottom', - weight: 1, - angle: 0 - }, style); - - ctx.save(); - ctx.translate(x, y); - ctx.rotate(style.angle); - ctx.fillStyle = style.color; - ctx.font = (style.weight > 1 ? "bold " : "") + (style.size*1.3) + "px sans-serif"; - ctx.textAlign = style.textAlign; - ctx.textBaseline = style.textBaseline; - ctx.fillText(text, 0, 0); - ctx.restore(); - }, - getBestTextAlign: function(angle, style) { - style = style || {textAlign: 'center', textBaseline: 'middle'}; - angle += Flotr.getTextAngleFromAlign(style); - - if (Math.abs(Math.cos(angle)) > 10e-3) - style.textAlign = (Math.cos(angle) > 0 ? 'right' : 'left'); - - if (Math.abs(Math.sin(angle)) > 10e-3) - style.textBaseline = (Math.sin(angle) > 0 ? 'top' : 'bottom'); - - return style; - }, - alignTable: { - 'right middle' : 0, - 'right top' : Math.PI/4, - 'center top' : Math.PI/2, - 'left top' : 3*(Math.PI/4), - 'left middle' : Math.PI, - 'left bottom' : -3*(Math.PI/4), - 'center bottom': -Math.PI/2, - 'right bottom' : -Math.PI/4, - 'center middle': 0 - }, - getTextAngleFromAlign: function(style) { - return Flotr.alignTable[style.textAlign+' '+style.textBaseline] || 0; - }, - noConflict : function () { - global.Flotr = previousFlotr; - return this; - } -}; - -global.Flotr = Flotr; - -})(); - -/** - * Flotr Defaults - */ -Flotr.defaultOptions = { - colors: ['#00A8F0', '#C0D800', '#CB4B4B', '#4DA74D', '#9440ED'], //=> The default colorscheme. When there are > 5 series, additional colors are generated. - ieBackgroundColor: '#FFFFFF', // Background color for excanvas clipping - title: null, // => The graph's title - subtitle: null, // => The graph's subtitle - shadowSize: 4, // => size of the 'fake' shadow - defaultType: null, // => default series type - HtmlText: true, // => wether to draw the text using HTML or on the canvas - fontColor: '#545454', // => default font color - fontSize: 7.5, // => canvas' text font size - resolution: 1, // => resolution of the graph, to have printer-friendly graphs ! - parseFloat: true, // => whether to preprocess data for floats (ie. if input is string) - preventDefault: true, // => preventDefault by default for mobile events. Turn off to enable scroll. - xaxis: { - ticks: null, // => format: either [1, 3] or [[1, 'a'], 3] - minorTicks: null, // => format: either [1, 3] or [[1, 'a'], 3] - showLabels: true, // => setting to true will show the axis ticks labels, hide otherwise - showMinorLabels: false,// => true to show the axis minor ticks labels, false to hide - labelsAngle: 0, // => labels' angle, in degrees - title: null, // => axis title - titleAngle: 0, // => axis title's angle, in degrees - noTicks: 5, // => number of ticks for automagically generated ticks - minorTickFreq: null, // => number of minor ticks between major ticks for autogenerated ticks - tickFormatter: Flotr.defaultTickFormatter, // => fn: number, Object -> string - tickDecimals: null, // => no. of decimals, null means auto - min: null, // => min. value to show, null means set automatically - max: null, // => max. value to show, null means set automatically - autoscale: false, // => Turns autoscaling on with true - autoscaleMargin: 0, // => margin in % to add if auto-setting min/max - color: null, // => color of the ticks - mode: 'normal', // => can be 'time' or 'normal' - timeFormat: null, - timeMode:'UTC', // => For UTC time ('local' for local time). - timeUnit:'millisecond',// => Unit for time (millisecond, second, minute, hour, day, month, year) - scaling: 'linear', // => Scaling, can be 'linear' or 'logarithmic' - base: Math.E, - titleAlign: 'center', - margin: true // => Turn off margins with false - }, - x2axis: {}, - yaxis: { - ticks: null, // => format: either [1, 3] or [[1, 'a'], 3] - minorTicks: null, // => format: either [1, 3] or [[1, 'a'], 3] - showLabels: true, // => setting to true will show the axis ticks labels, hide otherwise - showMinorLabels: false,// => true to show the axis minor ticks labels, false to hide - labelsAngle: 0, // => labels' angle, in degrees - title: null, // => axis title - titleAngle: 90, // => axis title's angle, in degrees - noTicks: 5, // => number of ticks for automagically generated ticks - minorTickFreq: null, // => number of minor ticks between major ticks for autogenerated ticks - tickFormatter: Flotr.defaultTickFormatter, // => fn: number, Object -> string - tickDecimals: null, // => no. of decimals, null means auto - min: null, // => min. value to show, null means set automatically - max: null, // => max. value to show, null means set automatically - autoscale: false, // => Turns autoscaling on with true - autoscaleMargin: 0, // => margin in % to add if auto-setting min/max - color: null, // => The color of the ticks - scaling: 'linear', // => Scaling, can be 'linear' or 'logarithmic' - base: Math.E, - titleAlign: 'center', - margin: true // => Turn off margins with false - }, - y2axis: { - titleAngle: 270 - }, - grid: { - color: '#545454', // => primary color used for outline and labels - backgroundColor: null, // => null for transparent, else color - backgroundImage: null, // => background image. String or object with src, left and top - watermarkAlpha: 0.4, // => - tickColor: '#DDDDDD', // => color used for the ticks - labelMargin: 3, // => margin in pixels - verticalLines: true, // => whether to show gridlines in vertical direction - minorVerticalLines: null, // => whether to show gridlines for minor ticks in vertical dir. - horizontalLines: true, // => whether to show gridlines in horizontal direction - minorHorizontalLines: null, // => whether to show gridlines for minor ticks in horizontal dir. - outlineWidth: 1, // => width of the grid outline/border in pixels - outline : 'nsew', // => walls of the outline to display - circular: false // => if set to true, the grid will be circular, must be used when radars are drawn - }, - mouse: { - track: false, // => true to track the mouse, no tracking otherwise - trackAll: false, - position: 'se', // => position of the value box (default south-east) - relative: false, // => next to the mouse cursor - trackFormatter: Flotr.defaultTrackFormatter, // => formats the values in the value box - margin: 5, // => margin in pixels of the valuebox - lineColor: '#FF3F19', // => line color of points that are drawn when mouse comes near a value of a series - trackDecimals: 1, // => decimals for the track values - sensibility: 2, // => the lower this number, the more precise you have to aim to show a value - trackY: true, // => whether or not to track the mouse in the y axis - radius: 3, // => radius of the track point - fillColor: null, // => color to fill our select bar with only applies to bar and similar graphs (only bars for now) - fillOpacity: 0.4 // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill - } -}; - -/** - * Flotr Color - */ - -(function () { - -var - _ = Flotr._; - -// Constructor -function Color (r, g, b, a) { - this.rgba = ['r','g','b','a']; - var x = 4; - while(-1<--x){ - this[this.rgba[x]] = arguments[x] || ((x==3) ? 1.0 : 0); - } - this.normalize(); -} - -// Constants -var COLOR_NAMES = { - aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255], - brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169], - darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47], - darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122], - darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130], - khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144], - lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255], - maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128], - violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0] -}; - -Color.prototype = { - scale: function(rf, gf, bf, af){ - var x = 4; - while (-1 < --x) { - if (!_.isUndefined(arguments[x])) this[this.rgba[x]] *= arguments[x]; - } - return this.normalize(); - }, - alpha: function(alpha) { - if (!_.isUndefined(alpha) && !_.isNull(alpha)) { - this.a = alpha; - } - return this.normalize(); - }, - clone: function(){ - return new Color(this.r, this.b, this.g, this.a); - }, - limit: function(val,minVal,maxVal){ - return Math.max(Math.min(val, maxVal), minVal); - }, - normalize: function(){ - var limit = this.limit; - this.r = limit(parseInt(this.r, 10), 0, 255); - this.g = limit(parseInt(this.g, 10), 0, 255); - this.b = limit(parseInt(this.b, 10), 0, 255); - this.a = limit(this.a, 0, 1); - return this; - }, - distance: function(color){ - if (!color) return; - color = new Color.parse(color); - var dist = 0, x = 3; - while(-1<--x){ - dist += Math.abs(this[this.rgba[x]] - color[this.rgba[x]]); - } - return dist; - }, - toString: function(){ - return (this.a >= 1.0) ? 'rgb('+[this.r,this.g,this.b].join(',')+')' : 'rgba('+[this.r,this.g,this.b,this.a].join(',')+')'; - }, - contrast: function () { - var - test = 1 - ( 0.299 * this.r + 0.587 * this.g + 0.114 * this.b) / 255; - return (test < 0.5 ? '#000000' : '#ffffff'); - } -}; - -_.extend(Color, { - /** - * Parses a color string and returns a corresponding Color. - * The different tests are in order of probability to improve speed. - * @param {String, Color} str - string thats representing a color - * @return {Color} returns a Color object or false - */ - parse: function(color){ - if (color instanceof Color) return color; - - var result; - - // #a0b1c2 - if((result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color))) - return new Color(parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)); - - // rgb(num,num,num) - if((result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color))) - return new Color(parseInt(result[1], 10), parseInt(result[2], 10), parseInt(result[3], 10)); - - // #fff - if((result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color))) - return new Color(parseInt(result[1]+result[1],16), parseInt(result[2]+result[2],16), parseInt(result[3]+result[3],16)); - - // rgba(num,num,num,num) - if((result = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(color))) - return new Color(parseInt(result[1], 10), parseInt(result[2], 10), parseInt(result[3], 10), parseFloat(result[4])); - - // rgb(num%,num%,num%) - if((result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(color))) - return new Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55); - - // rgba(num%,num%,num%,num) - if((result = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(color))) - return new Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55, parseFloat(result[4])); - - // Otherwise, we're most likely dealing with a named color. - var name = (color+'').replace(/^\s*([\S\s]*?)\s*$/, '$1').toLowerCase(); - if(name == 'transparent'){ - return new Color(255, 255, 255, 0); - } - return (result = COLOR_NAMES[name]) ? new Color(result[0], result[1], result[2]) : new Color(0, 0, 0, 0); - }, - - /** - * Process color and options into color style. - */ - processColor: function(color, options) { - - var opacity = options.opacity; - if (!color) return 'rgba(0, 0, 0, 0)'; - if (color instanceof Color) return color.alpha(opacity).toString(); - if (_.isString(color)) return Color.parse(color).alpha(opacity).toString(); - - var grad = color.colors ? color : {colors: color}; - - if (!options.ctx) { - if (!_.isArray(grad.colors)) return 'rgba(0, 0, 0, 0)'; - return Color.parse(_.isArray(grad.colors[0]) ? grad.colors[0][1] : grad.colors[0]).alpha(opacity).toString(); - } - grad = _.extend({start: 'top', end: 'bottom'}, grad); - - if (/top/i.test(grad.start)) options.x1 = 0; - if (/left/i.test(grad.start)) options.y1 = 0; - if (/bottom/i.test(grad.end)) options.x2 = 0; - if (/right/i.test(grad.end)) options.y2 = 0; - - var i, c, stop, gradient = options.ctx.createLinearGradient(options.x1, options.y1, options.x2, options.y2); - for (i = 0; i < grad.colors.length; i++) { - c = grad.colors[i]; - if (_.isArray(c)) { - stop = c[0]; - c = c[1]; - } - else stop = i / (grad.colors.length-1); - gradient.addColorStop(stop, Color.parse(c).alpha(opacity)); - } - return gradient; - } -}); - -Flotr.Color = Color; - -})(); - -/** - * Flotr Date - */ -Flotr.Date = { - - set : function (date, name, mode, value) { - mode = mode || 'UTC'; - name = 'set' + (mode === 'UTC' ? 'UTC' : '') + name; - date[name](value); - }, - - get : function (date, name, mode) { - mode = mode || 'UTC'; - name = 'get' + (mode === 'UTC' ? 'UTC' : '') + name; - return date[name](); - }, - - format: function(d, format, mode) { - if (!d) return; - - // We should maybe use an "official" date format spec, like PHP date() or ColdFusion - // http://fr.php.net/manual/en/function.date.php - // http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=functions_c-d_29.html - var - get = this.get, - tokens = { - h: get(d, 'Hours', mode).toString(), - H: leftPad(get(d, 'Hours', mode)), - M: leftPad(get(d, 'Minutes', mode)), - S: leftPad(get(d, 'Seconds', mode)), - s: get(d, 'Milliseconds', mode), - d: get(d, 'Date', mode).toString(), - m: (get(d, 'Month', mode) + 1).toString(), - y: get(d, 'FullYear', mode).toString(), - b: Flotr.Date.monthNames[get(d, 'Month', mode)] - }; - - function leftPad(n){ - n += ''; - return n.length == 1 ? "0" + n : n; - } - - var r = [], c, - escape = false; - - for (var i = 0; i < format.length; ++i) { - c = format.charAt(i); - - if (escape) { - r.push(tokens[c] || c); - escape = false; - } - else if (c == "%") - escape = true; - else - r.push(c); - } - return r.join(''); - }, - getFormat: function(time, span) { - var tu = Flotr.Date.timeUnits; - if (time < tu.second) return "%h:%M:%S.%s"; - else if (time < tu.minute) return "%h:%M:%S"; - else if (time < tu.day) return (span < 2 * tu.day) ? "%h:%M" : "%b %d %h:%M"; - else if (time < tu.month) return "%b %d"; - else if (time < tu.year) return (span < tu.year) ? "%b" : "%b %y"; - else return "%y"; - }, - formatter: function (v, axis) { - var - options = axis.options, - scale = Flotr.Date.timeUnits[options.timeUnit], - d = new Date(v * scale); - - // first check global format - if (axis.options.timeFormat) - return Flotr.Date.format(d, options.timeFormat, options.timeMode); - - var span = (axis.max - axis.min) * scale, - t = axis.tickSize * Flotr.Date.timeUnits[axis.tickUnit]; - - return Flotr.Date.format(d, Flotr.Date.getFormat(t, span), options.timeMode); - }, - generator: function(axis) { - - var - set = this.set, - get = this.get, - timeUnits = this.timeUnits, - spec = this.spec, - options = axis.options, - mode = options.timeMode, - scale = timeUnits[options.timeUnit], - min = axis.min * scale, - max = axis.max * scale, - delta = (max - min) / options.noTicks, - ticks = [], - tickSize = axis.tickSize, - tickUnit, - formatter, i; - - // Use custom formatter or time tick formatter - formatter = (options.tickFormatter === Flotr.defaultTickFormatter ? - this.formatter : options.tickFormatter - ); - - for (i = 0; i < spec.length - 1; ++i) { - var d = spec[i][0] * timeUnits[spec[i][1]]; - if (delta < (d + spec[i+1][0] * timeUnits[spec[i+1][1]]) / 2 && d >= tickSize) - break; - } - tickSize = spec[i][0]; - tickUnit = spec[i][1]; - - // special-case the possibility of several years - if (tickUnit == "year") { - tickSize = Flotr.getTickSize(options.noTicks*timeUnits.year, min, max, 0); - - // Fix for 0.5 year case - if (tickSize == 0.5) { - tickUnit = "month"; - tickSize = 6; - } - } - - axis.tickUnit = tickUnit; - axis.tickSize = tickSize; - - var step = tickSize * timeUnits[tickUnit]; - d = new Date(min); - - function setTick (name) { - set(d, name, mode, Flotr.floorInBase( - get(d, name, mode), tickSize - )); - } - - switch (tickUnit) { - case "millisecond": setTick('Milliseconds'); break; - case "second": setTick('Seconds'); break; - case "minute": setTick('Minutes'); break; - case "hour": setTick('Hours'); break; - case "month": setTick('Month'); break; - case "year": setTick('FullYear'); break; - } - - // reset smaller components - if (step >= timeUnits.second) set(d, 'Milliseconds', mode, 0); - if (step >= timeUnits.minute) set(d, 'Seconds', mode, 0); - if (step >= timeUnits.hour) set(d, 'Minutes', mode, 0); - if (step >= timeUnits.day) set(d, 'Hours', mode, 0); - if (step >= timeUnits.day * 4) set(d, 'Date', mode, 1); - if (step >= timeUnits.year) set(d, 'Month', mode, 0); - - var carry = 0, v = NaN, prev; - do { - prev = v; - v = d.getTime(); - ticks.push({ v: v / scale, label: formatter(v / scale, axis) }); - if (tickUnit == "month") { - if (tickSize < 1) { - /* a bit complicated - we'll divide the month up but we need to take care of fractions - so we don't end up in the middle of a day */ - set(d, 'Date', mode, 1); - var start = d.getTime(); - set(d, 'Month', mode, get(d, 'Month', mode) + 1); - var end = d.getTime(); - d.setTime(v + carry * timeUnits.hour + (end - start) * tickSize); - carry = get(d, 'Hours', mode); - set(d, 'Hours', mode, 0); - } - else - set(d, 'Month', mode, get(d, 'Month', mode) + tickSize); - } - else if (tickUnit == "year") { - set(d, 'FullYear', mode, get(d, 'FullYear', mode) + tickSize); - } - else - d.setTime(v + step); - - } while (v < max && v != prev); - - return ticks; - }, - timeUnits: { - millisecond: 1, - second: 1000, - minute: 1000 * 60, - hour: 1000 * 60 * 60, - day: 1000 * 60 * 60 * 24, - month: 1000 * 60 * 60 * 24 * 30, - year: 1000 * 60 * 60 * 24 * 365.2425 - }, - // the allowed tick sizes, after 1 year we use an integer algorithm - spec: [ - [1, "millisecond"], [20, "millisecond"], [50, "millisecond"], [100, "millisecond"], [200, "millisecond"], [500, "millisecond"], - [1, "second"], [2, "second"], [5, "second"], [10, "second"], [30, "second"], - [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], [30, "minute"], - [1, "hour"], [2, "hour"], [4, "hour"], [8, "hour"], [12, "hour"], - [1, "day"], [2, "day"], [3, "day"], - [0.25, "month"], [0.5, "month"], [1, "month"], [2, "month"], [3, "month"], [6, "month"], - [1, "year"] - ], - monthNames: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] -}; - -(function () { - -var _ = Flotr._; - -Flotr.DOM = { - addClass: function(element, name){ - var classList = (element.className ? element.className : ''); - if (_.include(classList.split(/\s+/g), name)) return; - element.className = (classList ? classList + ' ' : '') + name; - }, - /** - * Create an element. - */ - create: function(tag){ - return document.createElement(tag); - }, - node: function(html) { - var div = Flotr.DOM.create('div'), n; - div.innerHTML = html; - n = div.children[0]; - div.innerHTML = ''; - return n; - }, - /** - * Remove all children. - */ - empty: function(element){ - element.innerHTML = ''; - /* - if (!element) return; - _.each(element.childNodes, function (e) { - Flotr.DOM.empty(e); - element.removeChild(e); - }); - */ - }, - hide: function(element){ - Flotr.DOM.setStyles(element, {display:'none'}); - }, - /** - * Insert a child. - * @param {Element} element - * @param {Element|String} Element or string to be appended. - */ - insert: function(element, child){ - if(_.isString(child)) - element.innerHTML += child; - else if (_.isElement(child)) - element.appendChild(child); - }, - // @TODO find xbrowser implementation - opacity: function(element, opacity) { - element.style.opacity = opacity; - }, - position: function(element, p){ - if (!element.offsetParent) - return {left: (element.offsetLeft || 0), top: (element.offsetTop || 0)}; - - p = this.position(element.offsetParent); - p.left += element.offsetLeft; - p.top += element.offsetTop; - return p; - }, - removeClass: function(element, name) { - var classList = (element.className ? element.className : ''); - element.className = _.filter(classList.split(/\s+/g), function (c) { - if (c != name) return true; } - ).join(' '); - }, - setStyles: function(element, o) { - _.each(o, function (value, key) { - element.style[key] = value; - }); - }, - show: function(element){ - Flotr.DOM.setStyles(element, {display:''}); - }, - /** - * Return element size. - */ - size: function(element){ - return { - height : element.offsetHeight, - width : element.offsetWidth }; - } -}; - -})(); - -/** - * Flotr Event Adapter - */ -(function () { -var - F = Flotr, - bean = F.bean; -F.EventAdapter = { - observe: function(object, name, callback) { - bean.add(object, name, callback); - return this; - }, - fire: function(object, name, args) { - bean.fire(object, name, args); - if (typeof(Prototype) != 'undefined') - Event.fire(object, name, args); - // @TODO Someone who uses mootools, add mootools adapter for existing applciations. - return this; - }, - stopObserving: function(object, name, callback) { - bean.remove(object, name, callback); - return this; - }, - eventPointer: function(e) { - if (!F._.isUndefined(e.touches) && e.touches.length > 0) { - return { - x : e.touches[0].pageX, - y : e.touches[0].pageY - }; - } else if (!F._.isUndefined(e.changedTouches) && e.changedTouches.length > 0) { - return { - x : e.changedTouches[0].pageX, - y : e.changedTouches[0].pageY - }; - } else if (e.pageX || e.pageY) { - return { - x : e.pageX, - y : e.pageY - }; - } else if (e.clientX || e.clientY) { - var - d = document, - b = d.body, - de = d.documentElement; - return { - x: e.clientX + b.scrollLeft + de.scrollLeft, - y: e.clientY + b.scrollTop + de.scrollTop - }; - } - } -}; -})(); - -/** - * Text Utilities - */ -(function () { - -var - F = Flotr, - D = F.DOM, - _ = F._, - -Text = function (o) { - this.o = o; -}; - -Text.prototype = { - - dimensions : function (text, canvasStyle, htmlStyle, className) { - - if (!text) return { width : 0, height : 0 }; - - return (this.o.html) ? - this.html(text, this.o.element, htmlStyle, className) : - this.canvas(text, canvasStyle); - }, - - canvas : function (text, style) { - - if (!this.o.textEnabled) return; - style = style || {}; - - var - metrics = this.measureText(text, style), - width = metrics.width, - height = style.size || F.defaultOptions.fontSize, - angle = style.angle || 0, - cosAngle = Math.cos(angle), - sinAngle = Math.sin(angle), - widthPadding = 2, - heightPadding = 6, - bounds; - - bounds = { - width: Math.abs(cosAngle * width) + Math.abs(sinAngle * height) + widthPadding, - height: Math.abs(sinAngle * width) + Math.abs(cosAngle * height) + heightPadding - }; - - return bounds; - }, - - html : function (text, element, style, className) { - - var div = D.create('div'); - - D.setStyles(div, { 'position' : 'absolute', 'top' : '-10000px' }); - D.insert(div, '
' + text + '
'); - D.insert(this.o.element, div); - - return D.size(div); - }, - - measureText : function (text, style) { - - var - context = this.o.ctx, - metrics; - - if (!context.fillText || (F.isIphone && context.measure)) { - return { width : context.measure(text, style)}; - } - - style = _.extend({ - size: F.defaultOptions.fontSize, - weight: 1, - angle: 0 - }, style); - - context.save(); - context.font = (style.weight > 1 ? "bold " : "") + (style.size*1.3) + "px sans-serif"; - metrics = context.measureText(text); - context.restore(); - - return metrics; - } -}; - -Flotr.Text = Text; - -})(); - -/** - * Flotr Graph class that plots a graph on creation. - */ -(function () { - -var - D = Flotr.DOM, - E = Flotr.EventAdapter, - _ = Flotr._, - flotr = Flotr; -/** - * Flotr Graph constructor. - * @param {Element} el - element to insert the graph into - * @param {Object} data - an array or object of dataseries - * @param {Object} options - an object containing options - */ -Graph = function(el, data, options){ -// Let's see if we can get away with out this [JS] -// try { - this._setEl(el); - this._initMembers(); - this._initPlugins(); - - E.fire(this.el, 'flotr:beforeinit', [this]); - - this.data = data; - this.series = flotr.Series.getSeries(data); - this._initOptions(options); - this._initGraphTypes(); - this._initCanvas(); - this._text = new flotr.Text({ - element : this.el, - ctx : this.ctx, - html : this.options.HtmlText, - textEnabled : this.textEnabled - }); - E.fire(this.el, 'flotr:afterconstruct', [this]); - this._initEvents(); - - this.findDataRanges(); - this.calculateSpacing(); - - this.draw(_.bind(function() { - E.fire(this.el, 'flotr:afterinit', [this]); - }, this)); -/* - try { - } catch (e) { - try { - console.error(e); - } catch (e2) {} - }*/ -}; - -function observe (object, name, callback) { - E.observe.apply(this, arguments); - this._handles.push(arguments); - return this; -} - -Graph.prototype = { - - destroy: function () { - E.fire(this.el, 'flotr:destroy'); - _.each(this._handles, function (handle) { - E.stopObserving.apply(this, handle); - }); - this._handles = []; - this.el.graph = null; - }, - - observe : observe, - - /** - * @deprecated - */ - _observe : observe, - - processColor: function(color, options){ - var o = { x1: 0, y1: 0, x2: this.plotWidth, y2: this.plotHeight, opacity: 1, ctx: this.ctx }; - _.extend(o, options); - return flotr.Color.processColor(color, o); - }, - /** - * Function determines the min and max values for the xaxis and yaxis. - * - * TODO logarithmic range validation (consideration of 0) - */ - findDataRanges: function(){ - var a = this.axes, - xaxis, yaxis, range; - - _.each(this.series, function (series) { - range = series.getRange(); - if (range) { - xaxis = series.xaxis; - yaxis = series.yaxis; - xaxis.datamin = Math.min(range.xmin, xaxis.datamin); - xaxis.datamax = Math.max(range.xmax, xaxis.datamax); - yaxis.datamin = Math.min(range.ymin, yaxis.datamin); - yaxis.datamax = Math.max(range.ymax, yaxis.datamax); - xaxis.used = (xaxis.used || range.xused); - yaxis.used = (yaxis.used || range.yused); - } - }, this); - - // Check for empty data, no data case (none used) - if (!a.x.used && !a.x2.used) a.x.used = true; - if (!a.y.used && !a.y2.used) a.y.used = true; - - _.each(a, function (axis) { - axis.calculateRange(); - }); - - var - types = _.keys(flotr.graphTypes), - drawn = false; - - _.each(this.series, function (series) { - if (series.hide) return; - _.each(types, function (type) { - if (series[type] && series[type].show) { - this.extendRange(type, series); - drawn = true; - } - }, this); - if (!drawn) { - this.extendRange(this.options.defaultType, series); - } - }, this); - }, - - extendRange : function (type, series) { - if (this[type].extendRange) this[type].extendRange(series, series.data, series[type], this[type]); - if (this[type].extendYRange) this[type].extendYRange(series.yaxis, series.data, series[type], this[type]); - if (this[type].extendXRange) this[type].extendXRange(series.xaxis, series.data, series[type], this[type]); - }, - - /** - * Calculates axis label sizes. - */ - calculateSpacing: function(){ - - var a = this.axes, - options = this.options, - series = this.series, - margin = options.grid.labelMargin, - T = this._text, - x = a.x, - x2 = a.x2, - y = a.y, - y2 = a.y2, - maxOutset = options.grid.outlineWidth, - i, j, l, dim; - - // TODO post refactor, fix this - _.each(a, function (axis) { - axis.calculateTicks(); - axis.calculateTextDimensions(T, options); - }); - - // Title height - dim = T.dimensions( - options.title, - {size: options.fontSize*1.5}, - 'font-size:1em;font-weight:bold;', - 'flotr-title' - ); - this.titleHeight = dim.height; - - // Subtitle height - dim = T.dimensions( - options.subtitle, - {size: options.fontSize}, - 'font-size:smaller;', - 'flotr-subtitle' - ); - this.subtitleHeight = dim.height; - - for(j = 0; j < options.length; ++j){ - if (series[j].points.show){ - maxOutset = Math.max(maxOutset, series[j].points.radius + series[j].points.lineWidth/2); - } - } - - var p = this.plotOffset; - if (x.options.margin === false) { - p.bottom = 0; - p.top = 0; - } else { - p.bottom += (options.grid.circular ? 0 : (x.used && x.options.showLabels ? (x.maxLabel.height + margin) : 0)) + - (x.used && x.options.title ? (x.titleSize.height + margin) : 0) + maxOutset; - - p.top += (options.grid.circular ? 0 : (x2.used && x2.options.showLabels ? (x2.maxLabel.height + margin) : 0)) + - (x2.used && x2.options.title ? (x2.titleSize.height + margin) : 0) + this.subtitleHeight + this.titleHeight + maxOutset; - } - if (y.options.margin === false) { - p.left = 0; - p.right = 0; - } else { - p.left += (options.grid.circular ? 0 : (y.used && y.options.showLabels ? (y.maxLabel.width + margin) : 0)) + - (y.used && y.options.title ? (y.titleSize.width + margin) : 0) + maxOutset; - - p.right += (options.grid.circular ? 0 : (y2.used && y2.options.showLabels ? (y2.maxLabel.width + margin) : 0)) + - (y2.used && y2.options.title ? (y2.titleSize.width + margin) : 0) + maxOutset; - } - - p.top = Math.floor(p.top); // In order the outline not to be blured - - this.plotWidth = this.canvasWidth - p.left - p.right; - this.plotHeight = this.canvasHeight - p.bottom - p.top; - - // TODO post refactor, fix this - x.length = x2.length = this.plotWidth; - y.length = y2.length = this.plotHeight; - y.offset = y2.offset = this.plotHeight; - x.setScale(); - x2.setScale(); - y.setScale(); - y2.setScale(); - }, - /** - * Draws grid, labels, series and outline. - */ - draw: function(after) { - - var - context = this.ctx, - i; - - E.fire(this.el, 'flotr:beforedraw', [this.series, this]); - - if (this.series.length) { - - context.save(); - context.translate(this.plotOffset.left, this.plotOffset.top); - - for (i = 0; i < this.series.length; i++) { - if (!this.series[i].hide) this.drawSeries(this.series[i]); - } - - context.restore(); - this.clip(); - } - - E.fire(this.el, 'flotr:afterdraw', [this.series, this]); - if (after) after(); - }, - /** - * Actually draws the graph. - * @param {Object} series - series to draw - */ - drawSeries: function(series){ - - function drawChart (series, typeKey) { - var options = this.getOptions(series, typeKey); - this[typeKey].draw(options); - } - - var drawn = false; - series = series || this.series; - - _.each(flotr.graphTypes, function (type, typeKey) { - if (series[typeKey] && series[typeKey].show && this[typeKey]) { - drawn = true; - drawChart.call(this, series, typeKey); - } - }, this); - - if (!drawn) drawChart.call(this, series, this.options.defaultType); - }, - - getOptions : function (series, typeKey) { - var - type = series[typeKey], - graphType = this[typeKey], - xaxis = series.xaxis, - yaxis = series.yaxis, - options = { - context : this.ctx, - width : this.plotWidth, - height : this.plotHeight, - fontSize : this.options.fontSize, - fontColor : this.options.fontColor, - textEnabled : this.textEnabled, - htmlText : this.options.HtmlText, - text : this._text, // TODO Is this necessary? - element : this.el, - data : series.data, - color : series.color, - shadowSize : series.shadowSize, - xScale : xaxis.d2p, - yScale : yaxis.d2p, - xInverse : xaxis.p2d, - yInverse : yaxis.p2d - }; - - options = flotr.merge(type, options); - - // Fill - options.fillStyle = this.processColor( - type.fillColor || series.color, - {opacity: type.fillOpacity} - ); - - return options; - }, - /** - * Calculates the coordinates from a mouse event object. - * @param {Event} event - Mouse Event object. - * @return {Object} Object with coordinates of the mouse. - */ - getEventPosition: function (e){ - - var - d = document, - b = d.body, - de = d.documentElement, - axes = this.axes, - plotOffset = this.plotOffset, - lastMousePos = this.lastMousePos, - pointer = E.eventPointer(e), - dx = pointer.x - lastMousePos.pageX, - dy = pointer.y - lastMousePos.pageY, - r, rx, ry; - - if ('ontouchstart' in this.el) { - r = D.position(this.overlay); - rx = pointer.x - r.left - plotOffset.left; - ry = pointer.y - r.top - plotOffset.top; - } else { - r = this.overlay.getBoundingClientRect(); - rx = e.clientX - r.left - plotOffset.left - b.scrollLeft - de.scrollLeft; - ry = e.clientY - r.top - plotOffset.top - b.scrollTop - de.scrollTop; - } - - return { - x: axes.x.p2d(rx), - x2: axes.x2.p2d(rx), - y: axes.y.p2d(ry), - y2: axes.y2.p2d(ry), - relX: rx, - relY: ry, - dX: dx, - dY: dy, - absX: pointer.x, - absY: pointer.y, - pageX: pointer.x, - pageY: pointer.y - }; - }, - /** - * Observes the 'click' event and fires the 'flotr:click' event. - * @param {Event} event - 'click' Event object. - */ - clickHandler: function(event){ - if(this.ignoreClick){ - this.ignoreClick = false; - return this.ignoreClick; - } - E.fire(this.el, 'flotr:click', [this.getEventPosition(event), this]); - }, - /** - * Observes mouse movement over the graph area. Fires the 'flotr:mousemove' event. - * @param {Event} event - 'mousemove' Event object. - */ - mouseMoveHandler: function(event){ - if (this.mouseDownMoveHandler) return; - var pos = this.getEventPosition(event); - E.fire(this.el, 'flotr:mousemove', [event, pos, this]); - this.lastMousePos = pos; - }, - /** - * Observes the 'mousedown' event. - * @param {Event} event - 'mousedown' Event object. - */ - mouseDownHandler: function (event){ - - /* - // @TODO Context menu? - if(event.isRightClick()) { - event.stop(); - - var overlay = this.overlay; - overlay.hide(); - - function cancelContextMenu () { - overlay.show(); - E.stopObserving(document, 'mousemove', cancelContextMenu); - } - E.observe(document, 'mousemove', cancelContextMenu); - return; - } - */ - - if (this.mouseUpHandler) return; - this.mouseUpHandler = _.bind(function (e) { - E.stopObserving(document, 'mouseup', this.mouseUpHandler); - E.stopObserving(document, 'mousemove', this.mouseDownMoveHandler); - this.mouseDownMoveHandler = null; - this.mouseUpHandler = null; - // @TODO why? - //e.stop(); - E.fire(this.el, 'flotr:mouseup', [e, this]); - }, this); - this.mouseDownMoveHandler = _.bind(function (e) { - var pos = this.getEventPosition(e); - E.fire(this.el, 'flotr:mousemove', [event, pos, this]); - this.lastMousePos = pos; - }, this); - E.observe(document, 'mouseup', this.mouseUpHandler); - E.observe(document, 'mousemove', this.mouseDownMoveHandler); - E.fire(this.el, 'flotr:mousedown', [event, this]); - this.ignoreClick = false; - }, - drawTooltip: function(content, x, y, options) { - var mt = this.getMouseTrack(), - style = 'opacity:0.7;background-color:#000;color:#fff;display:none;position:absolute;padding:2px 8px;-moz-border-radius:4px;border-radius:4px;white-space:nowrap;', - p = options.position, - m = options.margin, - plotOffset = this.plotOffset; - - if(x !== null && y !== null){ - if (!options.relative) { // absolute to the canvas - if(p.charAt(0) == 'n') style += 'top:' + (m + plotOffset.top) + 'px;bottom:auto;'; - else if(p.charAt(0) == 's') style += 'bottom:' + (m + plotOffset.bottom) + 'px;top:auto;'; - if(p.charAt(1) == 'e') style += 'right:' + (m + plotOffset.right) + 'px;left:auto;'; - else if(p.charAt(1) == 'w') style += 'left:' + (m + plotOffset.left) + 'px;right:auto;'; - } - else { // relative to the mouse - if(p.charAt(0) == 'n') style += 'bottom:' + (m - plotOffset.top - y + this.canvasHeight) + 'px;top:auto;'; - else if(p.charAt(0) == 's') style += 'top:' + (m + plotOffset.top + y) + 'px;bottom:auto;'; - if(p.charAt(1) == 'e') style += 'left:' + (m + plotOffset.left + x) + 'px;right:auto;'; - else if(p.charAt(1) == 'w') style += 'right:' + (m - plotOffset.left - x + this.canvasWidth) + 'px;left:auto;'; - } - - mt.style.cssText = style; - D.empty(mt); - D.insert(mt, content); - D.show(mt); - } - else { - D.hide(mt); - } - }, - - clip: function (ctx) { - - var - o = this.plotOffset, - w = this.canvasWidth, - h = this.canvasHeight; - - ctx = ctx || this.ctx; - - if (flotr.isIE && flotr.isIE < 9) { - // Clipping for excanvas :-( - ctx.save(); - ctx.fillStyle = this.processColor(this.options.ieBackgroundColor); - ctx.fillRect(0, 0, w, o.top); - ctx.fillRect(0, 0, o.left, h); - ctx.fillRect(0, h - o.bottom, w, o.bottom); - ctx.fillRect(w - o.right, 0, o.right,h); - ctx.restore(); - } else { - ctx.clearRect(0, 0, w, o.top); - ctx.clearRect(0, 0, o.left, h); - ctx.clearRect(0, h - o.bottom, w, o.bottom); - ctx.clearRect(w - o.right, 0, o.right,h); - } - }, - - _initMembers: function() { - this._handles = []; - this.lastMousePos = {pageX: null, pageY: null }; - this.plotOffset = {left: 0, right: 0, top: 0, bottom: 0}; - this.ignoreClick = true; - this.prevHit = null; - }, - - _initGraphTypes: function() { - _.each(flotr.graphTypes, function(handler, graphType){ - this[graphType] = flotr.clone(handler); - }, this); - }, - - _initEvents: function () { - - var - el = this.el, - touchendHandler, movement, touchend; - - if ('ontouchstart' in el) { - - touchendHandler = _.bind(function (e) { - touchend = true; - E.stopObserving(document, 'touchend', touchendHandler); - E.fire(el, 'flotr:mouseup', [event, this]); - this.multitouches = null; - - if (!movement) { - this.clickHandler(e); - } - }, this); - - this.observe(this.overlay, 'touchstart', _.bind(function (e) { - movement = false; - touchend = false; - this.ignoreClick = false; - - if (e.touches && e.touches.length > 1) { - this.multitouches = e.touches; - } - - E.fire(el, 'flotr:mousedown', [event, this]); - this.observe(document, 'touchend', touchendHandler); - }, this)); - - this.observe(this.overlay, 'touchmove', _.bind(function (e) { - - var pos = this.getEventPosition(e); - - if (this.options.preventDefault) { - e.preventDefault(); - } - - movement = true; - - if (this.multitouches || (e.touches && e.touches.length > 1)) { - this.multitouches = e.touches; - } else { - if (!touchend) { - E.fire(el, 'flotr:mousemove', [event, pos, this]); - } - } - this.lastMousePos = pos; - }, this)); - - } else { - this. - observe(this.overlay, 'mousedown', _.bind(this.mouseDownHandler, this)). - observe(el, 'mousemove', _.bind(this.mouseMoveHandler, this)). - observe(this.overlay, 'click', _.bind(this.clickHandler, this)). - observe(el, 'mouseout', function () { - E.fire(el, 'flotr:mouseout'); - }); - } - }, - - /** - * Initializes the canvas and it's overlay canvas element. When the browser is IE, this makes use - * of excanvas. The overlay canvas is inserted for displaying interactions. After the canvas elements - * are created, the elements are inserted into the container element. - */ - _initCanvas: function(){ - var el = this.el, - o = this.options, - children = el.children, - removedChildren = [], - child, i, - size, style; - - // Empty the el - for (i = children.length; i--;) { - child = children[i]; - if (!this.canvas && child.className === 'flotr-canvas') { - this.canvas = child; - } else if (!this.overlay && child.className === 'flotr-overlay') { - this.overlay = child; - } else { - removedChildren.push(child); - } - } - for (i = removedChildren.length; i--;) { - el.removeChild(removedChildren[i]); - } - - D.setStyles(el, {position: 'relative'}); // For positioning labels and overlay. - size = {}; - size.width = el.clientWidth; - size.height = el.clientHeight; - - if(size.width <= 0 || size.height <= 0 || o.resolution <= 0){ - throw 'Invalid dimensions for plot, width = ' + size.width + ', height = ' + size.height + ', resolution = ' + o.resolution; - } - - // Main canvas for drawing graph types - this.canvas = getCanvas(this.canvas, 'canvas'); - // Overlay canvas for interactive features - this.overlay = getCanvas(this.overlay, 'overlay'); - this.ctx = getContext(this.canvas); - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - this.octx = getContext(this.overlay); - this.octx.clearRect(0, 0, this.overlay.width, this.overlay.height); - this.canvasHeight = size.height; - this.canvasWidth = size.width; - this.textEnabled = !!this.ctx.drawText || !!this.ctx.fillText; // Enable text functions - - function getCanvas(canvas, name){ - if(!canvas){ - canvas = D.create('canvas'); - if (typeof FlashCanvas != "undefined" && typeof canvas.getContext === 'function') { - FlashCanvas.initElement(canvas); - } - canvas.className = 'flotr-'+name; - canvas.style.cssText = 'position:absolute;left:0px;top:0px;'; - D.insert(el, canvas); - } - _.each(size, function(size, attribute){ - D.show(canvas); - if (name == 'canvas' && canvas.getAttribute(attribute) === size) { - return; - } - canvas.setAttribute(attribute, size * o.resolution); - canvas.style[attribute] = size + 'px'; - }); - canvas.context_ = null; // Reset the ExCanvas context - return canvas; - } - - function getContext(canvas){ - if(window.G_vmlCanvasManager) window.G_vmlCanvasManager.initElement(canvas); // For ExCanvas - var context = canvas.getContext('2d'); - if(!window.G_vmlCanvasManager) context.scale(o.resolution, o.resolution); - return context; - } - }, - - _initPlugins: function(){ - // TODO Should be moved to flotr and mixed in. - _.each(flotr.plugins, function(plugin, name){ - _.each(plugin.callbacks, function(fn, c){ - this.observe(this.el, c, _.bind(fn, this)); - }, this); - this[name] = flotr.clone(plugin); - _.each(this[name], function(fn, p){ - if (_.isFunction(fn)) - this[name][p] = _.bind(fn, this); - }, this); - }, this); - }, - - /** - * Sets options and initializes some variables and color specific values, used by the constructor. - * @param {Object} opts - options object - */ - _initOptions: function(opts){ - var options = flotr.clone(flotr.defaultOptions); - options.x2axis = _.extend(_.clone(options.xaxis), options.x2axis); - options.y2axis = _.extend(_.clone(options.yaxis), options.y2axis); - this.options = flotr.merge(opts || {}, options); - - if (this.options.grid.minorVerticalLines === null && - this.options.xaxis.scaling === 'logarithmic') { - this.options.grid.minorVerticalLines = true; - } - if (this.options.grid.minorHorizontalLines === null && - this.options.yaxis.scaling === 'logarithmic') { - this.options.grid.minorHorizontalLines = true; - } - - E.fire(this.el, 'flotr:afterinitoptions', [this]); - - this.axes = flotr.Axis.getAxes(this.options); - - // Initialize some variables used throughout this function. - var assignedColors = [], - colors = [], - ln = this.series.length, - neededColors = this.series.length, - oc = this.options.colors, - usedColors = [], - variation = 0, - c, i, j, s; - - // Collect user-defined colors from series. - for(i = neededColors - 1; i > -1; --i){ - c = this.series[i].color; - if(c){ - --neededColors; - if(_.isNumber(c)) assignedColors.push(c); - else usedColors.push(flotr.Color.parse(c)); - } - } - - // Calculate the number of colors that need to be generated. - for(i = assignedColors.length - 1; i > -1; --i) - neededColors = Math.max(neededColors, assignedColors[i] + 1); - - // Generate needed number of colors. - for(i = 0; colors.length < neededColors;){ - c = (oc.length == i) ? new flotr.Color(100, 100, 100) : flotr.Color.parse(oc[i]); - - // Make sure each serie gets a different color. - var sign = variation % 2 == 1 ? -1 : 1, - factor = 1 + sign * Math.ceil(variation / 2) * 0.2; - c.scale(factor, factor, factor); - - /** - * @todo if we're getting too close to something else, we should probably skip this one - */ - colors.push(c); - - if(++i >= oc.length){ - i = 0; - ++variation; - } - } - - // Fill the options with the generated colors. - for(i = 0, j = 0; i < ln; ++i){ - s = this.series[i]; - - // Assign the color. - if (!s.color){ - s.color = colors[j++].toString(); - }else if(_.isNumber(s.color)){ - s.color = colors[s.color].toString(); - } - - // Every series needs an axis - if (!s.xaxis) s.xaxis = this.axes.x; - if (s.xaxis == 1) s.xaxis = this.axes.x; - else if (s.xaxis == 2) s.xaxis = this.axes.x2; - - if (!s.yaxis) s.yaxis = this.axes.y; - if (s.yaxis == 1) s.yaxis = this.axes.y; - else if (s.yaxis == 2) s.yaxis = this.axes.y2; - - // Apply missing options to the series. - for (var t in flotr.graphTypes){ - s[t] = _.extend(_.clone(this.options[t]), s[t]); - } - s.mouse = _.extend(_.clone(this.options.mouse), s.mouse); - - if (_.isUndefined(s.shadowSize)) s.shadowSize = this.options.shadowSize; - } - }, - - _setEl: function(el) { - if (!el) throw 'The target container doesn\'t exist'; - else if (el.graph instanceof Graph) el.graph.destroy(); - else if (!el.clientWidth) throw 'The target container must be visible'; - - el.graph = this; - this.el = el; - } -}; - -Flotr.Graph = Graph; - -})(); - -/** - * Flotr Axis Library - */ - -(function () { - -var - _ = Flotr._, - LOGARITHMIC = 'logarithmic'; - -function Axis (o) { - - this.orientation = 1; - this.offset = 0; - this.datamin = Number.MAX_VALUE; - this.datamax = -Number.MAX_VALUE; - - _.extend(this, o); -} - - -// Prototype -Axis.prototype = { - - setScale : function () { - var - length = this.length, - max = this.max, - min = this.min, - offset = this.offset, - orientation = this.orientation, - options = this.options, - logarithmic = options.scaling === LOGARITHMIC, - scale; - - if (logarithmic) { - scale = length / (log(max, options.base) - log(min, options.base)); - } else { - scale = length / (max - min); - } - this.scale = scale; - - // Logarithmic? - if (logarithmic) { - this.d2p = function (dataValue) { - return offset + orientation * (log(dataValue, options.base) - log(min, options.base)) * scale; - } - this.p2d = function (pointValue) { - return exp((offset + orientation * pointValue) / scale + log(min, options.base), options.base); - } - } else { - this.d2p = function (dataValue) { - return offset + orientation * (dataValue - min) * scale; - } - this.p2d = function (pointValue) { - return (offset + orientation * pointValue) / scale + min; - } - } - }, - - calculateTicks : function () { - var options = this.options; - - this.ticks = []; - this.minorTicks = []; - - // User Ticks - if(options.ticks){ - this._cleanUserTicks(options.ticks, this.ticks); - this._cleanUserTicks(options.minorTicks || [], this.minorTicks); - } - else { - if (options.mode == 'time') { - this._calculateTimeTicks(); - } else if (options.scaling === 'logarithmic') { - this._calculateLogTicks(); - } else { - this._calculateTicks(); - } - } - - // Ticks to strings - _.each(this.ticks, function (tick) { tick.label += ''; }); - _.each(this.minorTicks, function (tick) { tick.label += ''; }); - }, - - /** - * Calculates the range of an axis to apply autoscaling. - */ - calculateRange: function () { - - if (!this.used) return; - - var axis = this, - o = axis.options, - min = o.min !== null ? o.min : axis.datamin, - max = o.max !== null ? o.max : axis.datamax, - margin = o.autoscaleMargin; - - if (o.scaling == 'logarithmic') { - if (min <= 0) min = axis.datamin; - - // Let it widen later on - if (max <= 0) max = min; - } - - if (max == min) { - var widen = max ? 0.01 : 1.00; - if (o.min === null) min -= widen; - if (o.max === null) max += widen; - } - - if (o.scaling === 'logarithmic') { - if (min < 0) min = max / o.base; // Could be the result of widening - - var maxexp = Math.log(max); - if (o.base != Math.E) maxexp /= Math.log(o.base); - maxexp = Math.ceil(maxexp); - - var minexp = Math.log(min); - if (o.base != Math.E) minexp /= Math.log(o.base); - minexp = Math.ceil(minexp); - - axis.tickSize = Flotr.getTickSize(o.noTicks, minexp, maxexp, o.tickDecimals === null ? 0 : o.tickDecimals); - - // Try to determine a suitable amount of miniticks based on the length of a decade - if (o.minorTickFreq === null) { - if (maxexp - minexp > 10) - o.minorTickFreq = 0; - else if (maxexp - minexp > 5) - o.minorTickFreq = 2; - else - o.minorTickFreq = 5; - } - } else { - axis.tickSize = Flotr.getTickSize(o.noTicks, min, max, o.tickDecimals); - } - - axis.min = min; - axis.max = max; //extendRange may use axis.min or axis.max, so it should be set before it is caled - - // Autoscaling. @todo This probably fails with log scale. Find a testcase and fix it - if(o.min === null && o.autoscale){ - axis.min -= axis.tickSize * margin; - // Make sure we don't go below zero if all values are positive. - if(axis.min < 0 && axis.datamin >= 0) axis.min = 0; - axis.min = axis.tickSize * Math.floor(axis.min / axis.tickSize); - } - - if(o.max === null && o.autoscale){ - axis.max += axis.tickSize * margin; - if(axis.max > 0 && axis.datamax <= 0 && axis.datamax != axis.datamin) axis.max = 0; - axis.max = axis.tickSize * Math.ceil(axis.max / axis.tickSize); - } - - if (axis.min == axis.max) axis.max = axis.min + 1; - }, - - calculateTextDimensions : function (T, options) { - - var maxLabel = '', - length, - i; - - if (this.options.showLabels) { - for (i = 0; i < this.ticks.length; ++i) { - length = this.ticks[i].label.length; - if (length > maxLabel.length){ - maxLabel = this.ticks[i].label; - } - } - } - - this.maxLabel = T.dimensions( - maxLabel, - {size:options.fontSize, angle: Flotr.toRad(this.options.labelsAngle)}, - 'font-size:smaller;', - 'flotr-grid-label' - ); - - this.titleSize = T.dimensions( - this.options.title, - {size:options.fontSize*1.2, angle: Flotr.toRad(this.options.titleAngle)}, - 'font-weight:bold;', - 'flotr-axis-title' - ); - }, - - _cleanUserTicks : function (ticks, axisTicks) { - - var axis = this, options = this.options, - v, i, label, tick; - - if(_.isFunction(ticks)) ticks = ticks({min : axis.min, max : axis.max}); - - for(i = 0; i < ticks.length; ++i){ - tick = ticks[i]; - if(typeof(tick) === 'object'){ - v = tick[0]; - label = (tick.length > 1) ? tick[1] : options.tickFormatter(v, {min : axis.min, max : axis.max}); - } else { - v = tick; - label = options.tickFormatter(v, {min : this.min, max : this.max}); - } - axisTicks[i] = { v: v, label: label }; - } - }, - - _calculateTimeTicks : function () { - this.ticks = Flotr.Date.generator(this); - }, - - _calculateLogTicks : function () { - - var axis = this, - o = axis.options, - v, - decadeStart; - - var max = Math.log(axis.max); - if (o.base != Math.E) max /= Math.log(o.base); - max = Math.ceil(max); - - var min = Math.log(axis.min); - if (o.base != Math.E) min /= Math.log(o.base); - min = Math.ceil(min); - - for (i = min; i < max; i += axis.tickSize) { - decadeStart = (o.base == Math.E) ? Math.exp(i) : Math.pow(o.base, i); - // Next decade begins here: - var decadeEnd = decadeStart * ((o.base == Math.E) ? Math.exp(axis.tickSize) : Math.pow(o.base, axis.tickSize)); - var stepSize = (decadeEnd - decadeStart) / o.minorTickFreq; - - axis.ticks.push({v: decadeStart, label: o.tickFormatter(decadeStart, {min : axis.min, max : axis.max})}); - for (v = decadeStart + stepSize; v < decadeEnd; v += stepSize) - axis.minorTicks.push({v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max})}); - } - - // Always show the value at the would-be start of next decade (end of this decade) - decadeStart = (o.base == Math.E) ? Math.exp(i) : Math.pow(o.base, i); - axis.ticks.push({v: decadeStart, label: o.tickFormatter(decadeStart, {min : axis.min, max : axis.max})}); - }, - - _calculateTicks : function () { - - var axis = this, - o = axis.options, - tickSize = axis.tickSize, - min = axis.min, - max = axis.max, - start = tickSize * Math.ceil(min / tickSize), // Round to nearest multiple of tick size. - decimals, - minorTickSize, - v, v2, - i, j; - - if (o.minorTickFreq) - minorTickSize = tickSize / o.minorTickFreq; - - // Then store all possible ticks. - for (i = 0; (v = v2 = start + i * tickSize) <= max; ++i){ - - // Round (this is always needed to fix numerical instability). - decimals = o.tickDecimals; - if (decimals === null) decimals = 1 - Math.floor(Math.log(tickSize) / Math.LN10); - if (decimals < 0) decimals = 0; - - v = v.toFixed(decimals); - axis.ticks.push({ v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max}) }); - - if (o.minorTickFreq) { - for (j = 0; j < o.minorTickFreq && (i * tickSize + j * minorTickSize) < max; ++j) { - v = v2 + j * minorTickSize; - axis.minorTicks.push({ v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max}) }); - } - } - } - - } -}; - - -// Static Methods -_.extend(Axis, { - getAxes : function (options) { - return { - x: new Axis({options: options.xaxis, n: 1, length: this.plotWidth}), - x2: new Axis({options: options.x2axis, n: 2, length: this.plotWidth}), - y: new Axis({options: options.yaxis, n: 1, length: this.plotHeight, offset: this.plotHeight, orientation: -1}), - y2: new Axis({options: options.y2axis, n: 2, length: this.plotHeight, offset: this.plotHeight, orientation: -1}) - }; - } -}); - - -// Helper Methods - - -function log (value, base) { - value = Math.log(Math.max(value, Number.MIN_VALUE)); - if (base !== Math.E) - value /= Math.log(base); - return value; -} - -function exp (value, base) { - return (base === Math.E) ? Math.exp(value) : Math.pow(base, value); -} - -Flotr.Axis = Axis; - -})(); - -/** - * Flotr Series Library - */ - -(function () { - -var - _ = Flotr._; - -function Series (o) { - _.extend(this, o); -} - -Series.prototype = { - - getRange: function () { - - var - data = this.data, - length = data.length, - xmin = Number.MAX_VALUE, - ymin = Number.MAX_VALUE, - xmax = -Number.MAX_VALUE, - ymax = -Number.MAX_VALUE, - xused = false, - yused = false, - x, y, i; - - if (length < 0 || this.hide) return false; - - for (i = 0; i < length; i++) { - x = data[i][0]; - y = data[i][1]; - if (x !== null) { - if (x < xmin) { xmin = x; xused = true; } - if (x > xmax) { xmax = x; xused = true; } - } - if (y !== null) { - if (y < ymin) { ymin = y; yused = true; } - if (y > ymax) { ymax = y; yused = true; } - } - } - - return { - xmin : xmin, - xmax : xmax, - ymin : ymin, - ymax : ymax, - xused : xused, - yused : yused - }; - } -}; - -_.extend(Series, { - /** - * Collects dataseries from input and parses the series into the right format. It returns an Array - * of Objects each having at least the 'data' key set. - * @param {Array, Object} data - Object or array of dataseries - * @return {Array} Array of Objects parsed into the right format ({(...,) data: [[x1,y1], [x2,y2], ...] (, ...)}) - */ - getSeries: function(data){ - return _.map(data, function(s){ - var series; - if (s.data) { - series = new Series(); - _.extend(series, s); - } else { - series = new Series({data:s}); - } - return series; - }); - } -}); - -Flotr.Series = Series; - -})(); - -/** Lines **/ -Flotr.addType('lines', { - options: { - show: false, // => setting to true will show lines, false will hide - lineWidth: 2, // => line width in pixels - fill: false, // => true to fill the area from the line to the x axis, false for (transparent) no fill - fillBorder: false, // => draw a border around the fill - fillColor: null, // => fill color - fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill - steps: false, // => draw steps - stacked: false // => setting to true will show stacked lines, false will show normal lines - }, - - stack : { - values : [] - }, - - /** - * Draws lines series in the canvas element. - * @param {Object} options - */ - draw : function (options) { - - var - context = options.context, - lineWidth = options.lineWidth, - shadowSize = options.shadowSize, - offset; - - context.save(); - context.lineJoin = 'round'; - - if (shadowSize) { - - context.lineWidth = shadowSize / 2; - offset = lineWidth / 2 + context.lineWidth / 2; - - // @TODO do this instead with a linear gradient - context.strokeStyle = "rgba(0,0,0,0.1)"; - this.plot(options, offset + shadowSize / 2, false); - - context.strokeStyle = "rgba(0,0,0,0.2)"; - this.plot(options, offset, false); - } - - context.lineWidth = lineWidth; - context.strokeStyle = options.color; - - this.plot(options, 0, true); - - context.restore(); - }, - - plot : function (options, shadowOffset, incStack) { - - var - context = options.context, - width = options.width, - height = options.height, - xScale = options.xScale, - yScale = options.yScale, - data = options.data, - stack = options.stacked ? this.stack : false, - length = data.length - 1, - prevx = null, - prevy = null, - zero = yScale(0), - start = null, - x1, x2, y1, y2, stack1, stack2, i; - - if (length < 1) return; - - context.beginPath(); - - for (i = 0; i < length; ++i) { - - // To allow empty values - if (data[i][1] === null || data[i+1][1] === null) { - if (options.fill) { - if (i > 0 && data[i][1]) { - context.stroke(); - fill(); - start = null; - context.closePath(); - context.beginPath(); - } - } - continue; - } - - // Zero is infinity for log scales - // TODO handle zero for logarithmic - // if (xa.options.scaling === 'logarithmic' && (data[i][0] <= 0 || data[i+1][0] <= 0)) continue; - // if (ya.options.scaling === 'logarithmic' && (data[i][1] <= 0 || data[i+1][1] <= 0)) continue; - - x1 = xScale(data[i][0]); - x2 = xScale(data[i+1][0]); - - if (start === null) start = data[i]; - - if (stack) { - - stack1 = stack.values[data[i][0]] || 0; - stack2 = stack.values[data[i+1][0]] || stack.values[data[i][0]] || 0; - - y1 = yScale(data[i][1] + stack1); - y2 = yScale(data[i+1][1] + stack2); - - if(incStack){ - stack.values[data[i][0]] = data[i][1]+stack1; - - if(i == length-1) - stack.values[data[i+1][0]] = data[i+1][1]+stack2; - } - } - else{ - y1 = yScale(data[i][1]); - y2 = yScale(data[i+1][1]); - } - - if ( - (y1 > height && y2 > height) || - (y1 < 0 && y2 < 0) || - (x1 < 0 && x2 < 0) || - (x1 > width && x2 > width) - ) continue; - - if((prevx != x1) || (prevy != y1 + shadowOffset)) - context.moveTo(x1, y1 + shadowOffset); - - prevx = x2; - prevy = y2 + shadowOffset; - if (options.steps) { - context.lineTo(prevx + shadowOffset / 2, y1 + shadowOffset); - context.lineTo(prevx + shadowOffset / 2, prevy); - } else { - context.lineTo(prevx, prevy); - } - } - - if (!options.fill || options.fill && !options.fillBorder) context.stroke(); - - fill(); - - function fill () { - // TODO stacked lines - if(!shadowOffset && options.fill && start){ - x1 = xScale(start[0]); - context.fillStyle = options.fillStyle; - context.lineTo(x2, zero); - context.lineTo(x1, zero); - context.lineTo(x1, yScale(start[1])); - context.fill(); - if (options.fillBorder) { - context.stroke(); - } - } - } - - context.closePath(); - }, - - // Perform any pre-render precalculations (this should be run on data first) - // - Pie chart total for calculating measures - // - Stacks for lines and bars - // precalculate : function () { - // } - // - // - // Get any bounds after pre calculation (axis can fetch this if does not have explicit min/max) - // getBounds : function () { - // } - // getMin : function () { - // } - // getMax : function () { - // } - // - // - // Padding around rendered elements - // getPadding : function () { - // } - - extendYRange : function (axis, data, options, lines) { - - var o = axis.options; - - // If stacked and auto-min - if (options.stacked && ((!o.max && o.max !== 0) || (!o.min && o.min !== 0))) { - - var - newmax = axis.max, - newmin = axis.min, - positiveSums = lines.positiveSums || {}, - negativeSums = lines.negativeSums || {}, - x, j; - - for (j = 0; j < data.length; j++) { - - x = data[j][0] + ''; - - // Positive - if (data[j][1] > 0) { - positiveSums[x] = (positiveSums[x] || 0) + data[j][1]; - newmax = Math.max(newmax, positiveSums[x]); - } - - // Negative - else { - negativeSums[x] = (negativeSums[x] || 0) + data[j][1]; - newmin = Math.min(newmin, negativeSums[x]); - } - } - - lines.negativeSums = negativeSums; - lines.positiveSums = positiveSums; - - axis.max = newmax; - axis.min = newmin; - } - - if (options.steps) { - - this.hit = function (options) { - var - data = options.data, - args = options.args, - yScale = options.yScale, - mouse = args[0], - length = data.length, - n = args[1], - x = options.xInverse(mouse.relX), - relY = mouse.relY, - i; - - for (i = 0; i < length - 1; i++) { - if (x >= data[i][0] && x <= data[i+1][0]) { - if (Math.abs(yScale(data[i][1]) - relY) < 8) { - n.x = data[i][0]; - n.y = data[i][1]; - n.index = i; - n.seriesIndex = options.index; - } - break; - } - } - }; - - this.drawHit = function (options) { - var - context = options.context, - args = options.args, - data = options.data, - xScale = options.xScale, - index = args.index, - x = xScale(args.x), - y = options.yScale(args.y), - x2; - - if (data.length - 1 > index) { - x2 = options.xScale(data[index + 1][0]); - context.save(); - context.strokeStyle = options.color; - context.lineWidth = options.lineWidth; - context.beginPath(); - context.moveTo(x, y); - context.lineTo(x2, y); - context.stroke(); - context.closePath(); - context.restore(); - } - }; - - this.clearHit = function (options) { - var - context = options.context, - args = options.args, - data = options.data, - xScale = options.xScale, - width = options.lineWidth, - index = args.index, - x = xScale(args.x), - y = options.yScale(args.y), - x2; - - if (data.length - 1 > index) { - x2 = options.xScale(data[index + 1][0]); - context.clearRect(x - width, y - width, x2 - x + 2 * width, 2 * width); - } - }; - } - } - -}); - -/** Bars **/ -Flotr.addType('bars', { - - options: { - show: false, // => setting to true will show bars, false will hide - lineWidth: 2, // => in pixels - barWidth: 1, // => in units of the x axis - fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill - fillColor: null, // => fill color - fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill - horizontal: false, // => horizontal bars (x and y inverted) - stacked: false, // => stacked bar charts - centered: true, // => center the bars to their x axis value - topPadding: 0.1, // => top padding in percent - grouped: false // => groups bars together which share x value, hit not supported. - }, - - stack : { - positive : [], - negative : [], - _positive : [], // Shadow - _negative : [] // Shadow - }, - - draw : function (options) { - var - context = options.context; - - this.current += 1; - - context.save(); - context.lineJoin = 'miter'; - // @TODO linewidth not interpreted the right way. - context.lineWidth = options.lineWidth; - context.strokeStyle = options.color; - if (options.fill) context.fillStyle = options.fillStyle; - - this.plot(options); - - context.restore(); - }, - - plot : function (options) { - - var - data = options.data, - context = options.context, - shadowSize = options.shadowSize, - i, geometry, left, top, width, height; - - if (data.length < 1) return; - - this.translate(context, options.horizontal); - - for (i = 0; i < data.length; i++) { - - geometry = this.getBarGeometry(data[i][0], data[i][1], options); - if (geometry === null) continue; - - left = geometry.left; - top = geometry.top; - width = geometry.width; - height = geometry.height; - - if (options.fill) context.fillRect(left, top, width, height); - if (shadowSize) { - context.save(); - context.fillStyle = 'rgba(0,0,0,0.05)'; - context.fillRect(left + shadowSize, top + shadowSize, width, height); - context.restore(); - } - if (options.lineWidth) { - context.strokeRect(left, top, width, height); - } - } - }, - - translate : function (context, horizontal) { - if (horizontal) { - context.rotate(-Math.PI / 2); - context.scale(-1, 1); - } - }, - - getBarGeometry : function (x, y, options) { - - var - horizontal = options.horizontal, - barWidth = options.barWidth, - centered = options.centered, - stack = options.stacked ? this.stack : false, - lineWidth = options.lineWidth, - bisection = centered ? barWidth / 2 : 0, - xScale = horizontal ? options.yScale : options.xScale, - yScale = horizontal ? options.xScale : options.yScale, - xValue = horizontal ? y : x, - yValue = horizontal ? x : y, - stackOffset = 0, - stackValue, left, right, top, bottom; - - if (options.grouped) { - this.current / this.groups; - xValue = xValue - bisection; - barWidth = barWidth / this.groups; - bisection = barWidth / 2; - xValue = xValue + barWidth * this.current - bisection; - } - - // Stacked bars - if (stack) { - stackValue = yValue > 0 ? stack.positive : stack.negative; - stackOffset = stackValue[xValue] || stackOffset; - stackValue[xValue] = stackOffset + yValue; - } - - left = xScale(xValue - bisection); - right = xScale(xValue + barWidth - bisection); - top = yScale(yValue + stackOffset); - bottom = yScale(stackOffset); - - // TODO for test passing... probably looks better without this - if (bottom < 0) bottom = 0; - - // TODO Skipping... - // if (right < xa.min || left > xa.max || top < ya.min || bottom > ya.max) continue; - - return (x === null || y === null) ? null : { - x : xValue, - y : yValue, - xScale : xScale, - yScale : yScale, - top : top, - left : Math.min(left, right) - lineWidth / 2, - width : Math.abs(right - left) - lineWidth, - height : bottom - top - }; - }, - - hit : function (options) { - var - data = options.data, - args = options.args, - mouse = args[0], - n = args[1], - x = options.xInverse(mouse.relX), - y = options.yInverse(mouse.relY), - hitGeometry = this.getBarGeometry(x, y, options), - width = hitGeometry.width / 2, - left = hitGeometry.left, - height = hitGeometry.y, - geometry, i; - - for (i = data.length; i--;) { - geometry = this.getBarGeometry(data[i][0], data[i][1], options); - if ( - // Height: - ( - // Positive Bars: - (height > 0 && height < geometry.y) || - // Negative Bars: - (height < 0 && height > geometry.y) - ) && - // Width: - (Math.abs(left - geometry.left) < width) - ) { - n.x = data[i][0]; - n.y = data[i][1]; - n.index = i; - n.seriesIndex = options.index; - } - } - }, - - drawHit : function (options) { - // TODO hits for stacked bars; implement using calculateStack option? - var - context = options.context, - args = options.args, - geometry = this.getBarGeometry(args.x, args.y, options), - left = geometry.left, - top = geometry.top, - width = geometry.width, - height = geometry.height; - - context.save(); - context.strokeStyle = options.color; - context.lineWidth = options.lineWidth; - this.translate(context, options.horizontal); - - // Draw highlight - context.beginPath(); - context.moveTo(left, top + height); - context.lineTo(left, top); - context.lineTo(left + width, top); - context.lineTo(left + width, top + height); - if (options.fill) { - context.fillStyle = options.fillStyle; - context.fill(); - } - context.stroke(); - context.closePath(); - - context.restore(); - }, - - clearHit: function (options) { - var - context = options.context, - args = options.args, - geometry = this.getBarGeometry(args.x, args.y, options), - left = geometry.left, - width = geometry.width, - top = geometry.top, - height = geometry.height, - lineWidth = 2 * options.lineWidth; - - context.save(); - this.translate(context, options.horizontal); - context.clearRect( - left - lineWidth, - Math.min(top, top + height) - lineWidth, - width + 2 * lineWidth, - Math.abs(height) + 2 * lineWidth - ); - context.restore(); - }, - - extendXRange : function (axis, data, options, bars) { - this._extendRange(axis, data, options, bars); - this.groups = (this.groups + 1) || 1; - this.current = 0; - }, - - extendYRange : function (axis, data, options, bars) { - this._extendRange(axis, data, options, bars); - }, - _extendRange: function (axis, data, options, bars) { - - var - max = axis.options.max; - - if (_.isNumber(max) || _.isString(max)) return; - - var - newmin = axis.min, - newmax = axis.max, - horizontal = options.horizontal, - orientation = axis.orientation, - positiveSums = this.positiveSums || {}, - negativeSums = this.negativeSums || {}, - value, datum, index, j; - - // Sides of bars - if ((orientation == 1 && !horizontal) || (orientation == -1 && horizontal)) { - if (options.centered) { - newmax = Math.max(axis.datamax + options.barWidth, newmax); - newmin = Math.min(axis.datamin - options.barWidth, newmin); - } - } - - if (options.stacked && - ((orientation == 1 && horizontal) || (orientation == -1 && !horizontal))){ - - for (j = data.length; j--;) { - value = data[j][(orientation == 1 ? 1 : 0)]+''; - datum = data[j][(orientation == 1 ? 0 : 1)]; - - // Positive - if (datum > 0) { - positiveSums[value] = (positiveSums[value] || 0) + datum; - newmax = Math.max(newmax, positiveSums[value]); - } - - // Negative - else { - negativeSums[value] = (negativeSums[value] || 0) + datum; - newmin = Math.min(newmin, negativeSums[value]); - } - } - } - - // End of bars - if ((orientation == 1 && horizontal) || (orientation == -1 && !horizontal)) { - if (options.topPadding && (axis.max === axis.datamax || (options.stacked && this.stackMax !== newmax))) { - newmax += options.topPadding * (newmax - newmin); - } - } - - this.stackMin = newmin; - this.stackMax = newmax; - this.negativeSums = negativeSums; - this.positiveSums = positiveSums; - - axis.max = newmax; - axis.min = newmin; - } - -}); - -/** Bubbles **/ -Flotr.addType('bubbles', { - options: { - show: false, // => setting to true will show radar chart, false will hide - lineWidth: 2, // => line width in pixels - fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill - fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill - baseRadius: 2 // => ratio of the radar, against the plot size - }, - draw : function (options) { - var - context = options.context, - shadowSize = options.shadowSize; - - context.save(); - context.lineWidth = options.lineWidth; - - // Shadows - context.fillStyle = 'rgba(0,0,0,0.05)'; - context.strokeStyle = 'rgba(0,0,0,0.05)'; - this.plot(options, shadowSize / 2); - context.strokeStyle = 'rgba(0,0,0,0.1)'; - this.plot(options, shadowSize / 4); - - // Chart - context.strokeStyle = options.color; - context.fillStyle = options.fillStyle; - this.plot(options); - - context.restore(); - }, - plot : function (options, offset) { - - var - data = options.data, - context = options.context, - geometry, - i, x, y, z; - - offset = offset || 0; - - for (i = 0; i < data.length; ++i){ - - geometry = this.getGeometry(data[i], options); - - context.beginPath(); - context.arc(geometry.x + offset, geometry.y + offset, geometry.z, 0, 2 * Math.PI, true); - context.stroke(); - if (options.fill) context.fill(); - context.closePath(); - } - }, - getGeometry : function (point, options) { - return { - x : options.xScale(point[0]), - y : options.yScale(point[1]), - z : point[2] * options.baseRadius - }; - }, - hit : function (options) { - var - data = options.data, - args = options.args, - mouse = args[0], - n = args[1], - relX = mouse.relX, - relY = mouse.relY, - distance, - geometry, - dx, dy; - - n.best = n.best || Number.MAX_VALUE; - - for (i = data.length; i--;) { - geometry = this.getGeometry(data[i], options); - - dx = geometry.x - relX; - dy = geometry.y - relY; - distance = Math.sqrt(dx * dx + dy * dy); - - if (distance < geometry.z && geometry.z < n.best) { - n.x = data[i][0]; - n.y = data[i][1]; - n.index = i; - n.seriesIndex = options.index; - n.best = geometry.z; - } - } - }, - drawHit : function (options) { - - var - context = options.context, - geometry = this.getGeometry(options.data[options.args.index], options); - - context.save(); - context.lineWidth = options.lineWidth; - context.fillStyle = options.fillStyle; - context.strokeStyle = options.color; - context.beginPath(); - context.arc(geometry.x, geometry.y, geometry.z, 0, 2 * Math.PI, true); - context.fill(); - context.stroke(); - context.closePath(); - context.restore(); - }, - clearHit : function (options) { - - var - context = options.context, - geometry = this.getGeometry(options.data[options.args.index], options), - offset = geometry.z + options.lineWidth; - - context.save(); - context.clearRect( - geometry.x - offset, - geometry.y - offset, - 2 * offset, - 2 * offset - ); - context.restore(); - } - // TODO Add a hit calculation method (like pie) -}); - -/** Candles **/ -Flotr.addType('candles', { - options: { - show: false, // => setting to true will show candle sticks, false will hide - lineWidth: 1, // => in pixels - wickLineWidth: 1, // => in pixels - candleWidth: 0.6, // => in units of the x axis - fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill - upFillColor: '#00A8F0',// => up sticks fill color - downFillColor: '#CB4B4B',// => down sticks fill color - fillOpacity: 0.5, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill - // TODO Test this barcharts option. - barcharts: false // => draw as barcharts (not standard bars but financial barcharts) - }, - - draw : function (options) { - - var - context = options.context; - - context.save(); - context.lineJoin = 'miter'; - context.lineCap = 'butt'; - // @TODO linewidth not interpreted the right way. - context.lineWidth = options.wickLineWidth || options.lineWidth; - - this.plot(options); - - context.restore(); - }, - - plot : function (options) { - - var - data = options.data, - context = options.context, - xScale = options.xScale, - yScale = options.yScale, - width = options.candleWidth / 2, - shadowSize = options.shadowSize, - lineWidth = options.lineWidth, - wickLineWidth = options.wickLineWidth, - pixelOffset = (wickLineWidth % 2) / 2, - color, - datum, x, y, - open, high, low, close, - left, right, bottom, top, bottom2, top2, - i; - - if (data.length < 1) return; - - for (i = 0; i < data.length; i++) { - datum = data[i]; - x = datum[0]; - open = datum[1]; - high = datum[2]; - low = datum[3]; - close = datum[4]; - left = xScale(x - width); - right = xScale(x + width); - bottom = yScale(low); - top = yScale(high); - bottom2 = yScale(Math.min(open, close)); - top2 = yScale(Math.max(open, close)); - - /* - // TODO skipping - if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max) - continue; - */ - - color = options[open > close ? 'downFillColor' : 'upFillColor']; - - // Fill the candle. - // TODO Test the barcharts option - if (options.fill && !options.barcharts) { - context.fillStyle = 'rgba(0,0,0,0.05)'; - context.fillRect(left + shadowSize, top2 + shadowSize, right - left, bottom2 - top2); - context.save(); - context.globalAlpha = options.fillOpacity; - context.fillStyle = color; - context.fillRect(left, top2 + lineWidth, right - left, bottom2 - top2); - context.restore(); - } - - // Draw candle outline/border, high, low. - if (lineWidth || wickLineWidth) { - - x = Math.floor((left + right) / 2) + pixelOffset; - - context.strokeStyle = color; - context.beginPath(); - - // TODO Again with the bartcharts - if (options.barcharts) { - - context.moveTo(x, Math.floor(top + width)); - context.lineTo(x, Math.floor(bottom + width)); - - y = Math.floor(open + width) + 0.5; - context.moveTo(Math.floor(left) + pixelOffset, y); - context.lineTo(x, y); - - y = Math.floor(close + width) + 0.5; - context.moveTo(Math.floor(right) + pixelOffset, y); - context.lineTo(x, y); - } else { - context.strokeRect(left, top2 + lineWidth, right - left, bottom2 - top2); - - context.moveTo(x, Math.floor(top2 + lineWidth)); - context.lineTo(x, Math.floor(top + lineWidth)); - context.moveTo(x, Math.floor(bottom2 + lineWidth)); - context.lineTo(x, Math.floor(bottom + lineWidth)); - } - - context.closePath(); - context.stroke(); - } - } - }, - extendXRange: function (axis, data, options) { - if (axis.options.max === null) { - axis.max = Math.max(axis.datamax + 0.5, axis.max); - axis.min = Math.min(axis.datamin - 0.5, axis.min); - } - } -}); - -/** Gantt - * Base on data in form [s,y,d] where: - * y - executor or simply y value - * s - task start value - * d - task duration - * **/ -Flotr.addType('gantt', { - options: { - show: false, // => setting to true will show gantt, false will hide - lineWidth: 2, // => in pixels - barWidth: 1, // => in units of the x axis - fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill - fillColor: null, // => fill color - fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill - centered: true // => center the bars to their x axis value - }, - /** - * Draws gantt series in the canvas element. - * @param {Object} series - Series with options.gantt.show = true. - */ - draw: function(series) { - var ctx = this.ctx, - bw = series.gantt.barWidth, - lw = Math.min(series.gantt.lineWidth, bw); - - ctx.save(); - ctx.translate(this.plotOffset.left, this.plotOffset.top); - ctx.lineJoin = 'miter'; - - /** - * @todo linewidth not interpreted the right way. - */ - ctx.lineWidth = lw; - ctx.strokeStyle = series.color; - - ctx.save(); - this.gantt.plotShadows(series, bw, 0, series.gantt.fill); - ctx.restore(); - - if(series.gantt.fill){ - var color = series.gantt.fillColor || series.color; - ctx.fillStyle = this.processColor(color, {opacity: series.gantt.fillOpacity}); - } - - this.gantt.plot(series, bw, 0, series.gantt.fill); - ctx.restore(); - }, - plot: function(series, barWidth, offset, fill){ - var data = series.data; - if(data.length < 1) return; - - var xa = series.xaxis, - ya = series.yaxis, - ctx = this.ctx, i; - - for(i = 0; i < data.length; i++){ - var y = data[i][0], - s = data[i][1], - d = data[i][2], - drawLeft = true, drawTop = true, drawRight = true; - - if (s === null || d === null) continue; - - var left = s, - right = s + d, - bottom = y - (series.gantt.centered ? barWidth/2 : 0), - top = y + barWidth - (series.gantt.centered ? barWidth/2 : 0); - - if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max) - continue; - - if(left < xa.min){ - left = xa.min; - drawLeft = false; - } - - if(right > xa.max){ - right = xa.max; - if (xa.lastSerie != series) - drawTop = false; - } - - if(bottom < ya.min) - bottom = ya.min; - - if(top > ya.max){ - top = ya.max; - if (ya.lastSerie != series) - drawTop = false; - } - - /** - * Fill the bar. - */ - if(fill){ - ctx.beginPath(); - ctx.moveTo(xa.d2p(left), ya.d2p(bottom) + offset); - ctx.lineTo(xa.d2p(left), ya.d2p(top) + offset); - ctx.lineTo(xa.d2p(right), ya.d2p(top) + offset); - ctx.lineTo(xa.d2p(right), ya.d2p(bottom) + offset); - ctx.fill(); - ctx.closePath(); - } - - /** - * Draw bar outline/border. - */ - if(series.gantt.lineWidth && (drawLeft || drawRight || drawTop)){ - ctx.beginPath(); - ctx.moveTo(xa.d2p(left), ya.d2p(bottom) + offset); - - ctx[drawLeft ?'lineTo':'moveTo'](xa.d2p(left), ya.d2p(top) + offset); - ctx[drawTop ?'lineTo':'moveTo'](xa.d2p(right), ya.d2p(top) + offset); - ctx[drawRight?'lineTo':'moveTo'](xa.d2p(right), ya.d2p(bottom) + offset); - - ctx.stroke(); - ctx.closePath(); - } - } - }, - plotShadows: function(series, barWidth, offset){ - var data = series.data; - if(data.length < 1) return; - - var i, y, s, d, - xa = series.xaxis, - ya = series.yaxis, - ctx = this.ctx, - sw = this.options.shadowSize; - - for(i = 0; i < data.length; i++){ - y = data[i][0]; - s = data[i][1]; - d = data[i][2]; - - if (s === null || d === null) continue; - - var left = s, - right = s + d, - bottom = y - (series.gantt.centered ? barWidth/2 : 0), - top = y + barWidth - (series.gantt.centered ? barWidth/2 : 0); - - if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max) - continue; - - if(left < xa.min) left = xa.min; - if(right > xa.max) right = xa.max; - if(bottom < ya.min) bottom = ya.min; - if(top > ya.max) top = ya.max; - - var width = xa.d2p(right)-xa.d2p(left)-((xa.d2p(right)+sw <= this.plotWidth) ? 0 : sw); - var height = ya.d2p(bottom)-ya.d2p(top)-((ya.d2p(bottom)+sw <= this.plotHeight) ? 0 : sw ); - - ctx.fillStyle = 'rgba(0,0,0,0.05)'; - ctx.fillRect(Math.min(xa.d2p(left)+sw, this.plotWidth), Math.min(ya.d2p(top)+sw, this.plotHeight), width, height); - } - }, - extendXRange: function(axis) { - if(axis.options.max === null){ - var newmin = axis.min, - newmax = axis.max, - i, j, x, s, g, - stackedSumsPos = {}, - stackedSumsNeg = {}, - lastSerie = null; - - for(i = 0; i < this.series.length; ++i){ - s = this.series[i]; - g = s.gantt; - - if(g.show && s.xaxis == axis) { - for (j = 0; j < s.data.length; j++) { - if (g.show) { - y = s.data[j][0]+''; - stackedSumsPos[y] = Math.max((stackedSumsPos[y] || 0), s.data[j][1]+s.data[j][2]); - lastSerie = s; - } - } - for (j in stackedSumsPos) { - newmax = Math.max(stackedSumsPos[j], newmax); - } - } - } - axis.lastSerie = lastSerie; - axis.max = newmax; - axis.min = newmin; - } - }, - extendYRange: function(axis){ - if(axis.options.max === null){ - var newmax = Number.MIN_VALUE, - newmin = Number.MAX_VALUE, - i, j, s, g, - stackedSumsPos = {}, - stackedSumsNeg = {}, - lastSerie = null; - - for(i = 0; i < this.series.length; ++i){ - s = this.series[i]; - g = s.gantt; - - if (g.show && !s.hide && s.yaxis == axis) { - var datamax = Number.MIN_VALUE, datamin = Number.MAX_VALUE; - for(j=0; j < s.data.length; j++){ - datamax = Math.max(datamax,s.data[j][0]); - datamin = Math.min(datamin,s.data[j][0]); - } - - if (g.centered) { - newmax = Math.max(datamax + 0.5, newmax); - newmin = Math.min(datamin - 0.5, newmin); - } - else { - newmax = Math.max(datamax + 1, newmax); - newmin = Math.min(datamin, newmin); - } - // For normal horizontal bars - if (g.barWidth + datamax > newmax){ - newmax = axis.max + g.barWidth; - } - } - } - axis.lastSerie = lastSerie; - axis.max = newmax; - axis.min = newmin; - axis.tickSize = Flotr.getTickSize(axis.options.noTicks, newmin, newmax, axis.options.tickDecimals); - } - } -}); - -/** Markers **/ -/** - * Formats the marker labels. - * @param {Object} obj - Marker value Object {x:..,y:..} - * @return {String} Formatted marker string - */ -(function () { - -Flotr.defaultMarkerFormatter = function(obj){ - return (Math.round(obj.y*100)/100)+''; -}; - -Flotr.addType('markers', { - options: { - show: false, // => setting to true will show markers, false will hide - lineWidth: 1, // => line width of the rectangle around the marker - color: '#000000', // => text color - fill: false, // => fill or not the marekers' rectangles - fillColor: "#FFFFFF", // => fill color - fillOpacity: 0.4, // => fill opacity - stroke: false, // => draw the rectangle around the markers - position: 'ct', // => the markers position (vertical align: b, m, t, horizontal align: l, c, r) - verticalMargin: 0, // => the margin between the point and the text. - labelFormatter: Flotr.defaultMarkerFormatter, - fontSize: Flotr.defaultOptions.fontSize, - stacked: false, // => true if markers should be stacked - stackingType: 'b', // => define staching behavior, (b- bars like, a - area like) (see Issue 125 for details) - horizontal: false // => true if markers should be horizontal (For now only in a case on horizontal stacked bars, stacks should be calculated horizontaly) - }, - - // TODO test stacked markers. - stack : { - positive : [], - negative : [], - values : [] - }, - - draw : function (options) { - - var - data = options.data, - context = options.context, - stack = options.stacked ? options.stack : false, - stackType = options.stackingType, - stackOffsetNeg, - stackOffsetPos, - stackOffset, - i, x, y, label; - - context.save(); - context.lineJoin = 'round'; - context.lineWidth = options.lineWidth; - context.strokeStyle = 'rgba(0,0,0,0.5)'; - context.fillStyle = options.fillStyle; - - function stackPos (a, b) { - stackOffsetPos = stack.negative[a] || 0; - stackOffsetNeg = stack.positive[a] || 0; - if (b > 0) { - stack.positive[a] = stackOffsetPos + b; - return stackOffsetPos + b; - } else { - stack.negative[a] = stackOffsetNeg + b; - return stackOffsetNeg + b; - } - } - - for (i = 0; i < data.length; ++i) { - - x = data[i][0]; - y = data[i][1]; - - if (stack) { - if (stackType == 'b') { - if (options.horizontal) y = stackPos(y, x); - else x = stackPos(x, y); - } else if (stackType == 'a') { - stackOffset = stack.values[x] || 0; - stack.values[x] = stackOffset + y; - y = stackOffset + y; - } - } - - label = options.labelFormatter({x: x, y: y, index: i, data : data}); - this.plot(options.xScale(x), options.yScale(y), label, options); - } - context.restore(); - }, - plot: function(x, y, label, options) { - var context = options.context; - if (isImage(label) && !label.complete) { - throw 'Marker image not loaded.'; - } else { - this._plot(x, y, label, options); - } - }, - - _plot: function(x, y, label, options) { - var context = options.context, - margin = 2, - left = x, - top = y, - dim; - - if (isImage(label)) - dim = {height : label.height, width: label.width}; - else - dim = options.text.canvas(label); - - dim.width = Math.floor(dim.width+margin*2); - dim.height = Math.floor(dim.height+margin*2); - - if (options.position.indexOf('c') != -1) left -= dim.width/2 + margin; - else if (options.position.indexOf('l') != -1) left -= dim.width; - - if (options.position.indexOf('m') != -1) top -= dim.height/2 + margin; - else if (options.position.indexOf('t') != -1) top -= dim.height + options.verticalMargin; - else top += options.verticalMargin; - - left = Math.floor(left)+0.5; - top = Math.floor(top)+0.5; - - if(options.fill) - context.fillRect(left, top, dim.width, dim.height); - - if(options.stroke) - context.strokeRect(left, top, dim.width, dim.height); - - if (isImage(label)) - context.drawImage(label, left+margin, top+margin); - else - Flotr.drawText(context, label, left+margin, top+margin, {textBaseline: 'top', textAlign: 'left', size: options.fontSize, color: options.color}); - } -}); - -function isImage (i) { - return typeof i === 'object' && i.constructor && (Image ? true : i.constructor === Image); -} - -})(); - -/** - * Pie - * - * Formats the pies labels. - * @param {Object} slice - Slice object - * @return {String} Formatted pie label string - */ -(function () { - -var - _ = Flotr._; - -Flotr.defaultPieLabelFormatter = function (total, value) { - return (100 * value / total).toFixed(2)+'%'; -}; - -Flotr.addType('pie', { - options: { - show: false, // => setting to true will show bars, false will hide - lineWidth: 1, // => in pixels - fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill - fillColor: null, // => fill color - fillOpacity: 0.6, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill - explode: 6, // => the number of pixels the splices will be far from the center - sizeRatio: 0.6, // => the size ratio of the pie relative to the plot - startAngle: Math.PI/4, // => the first slice start angle - labelFormatter: Flotr.defaultPieLabelFormatter, - pie3D: false, // => whether to draw the pie in 3 dimenstions or not (ineffective) - pie3DviewAngle: (Math.PI/2 * 0.8), - pie3DspliceThickness: 20, - epsilon: 0.1 // => how close do you have to get to hit empty slice - }, - - draw : function (options) { - - // TODO 3D charts what? - - var - data = options.data, - context = options.context, - canvas = context.canvas, - lineWidth = options.lineWidth, - shadowSize = options.shadowSize, - sizeRatio = options.sizeRatio, - height = options.height, - width = options.width, - explode = options.explode, - color = options.color, - fill = options.fill, - fillStyle = options.fillStyle, - radius = Math.min(canvas.width, canvas.height) * sizeRatio / 2, - value = data[0][1], - html = [], - vScale = 1,//Math.cos(series.pie.viewAngle); - measure = Math.PI * 2 * value / this.total, - startAngle = this.startAngle || (2 * Math.PI * options.startAngle), // TODO: this initial startAngle is already in radians (fixing will be test-unstable) - endAngle = startAngle + measure, - bisection = startAngle + measure / 2, - label = options.labelFormatter(this.total, value), - //plotTickness = Math.sin(series.pie.viewAngle)*series.pie.spliceThickness / vScale; - explodeCoeff = explode + radius + 4, - distX = Math.cos(bisection) * explodeCoeff, - distY = Math.sin(bisection) * explodeCoeff, - textAlign = distX < 0 ? 'right' : 'left', - textBaseline = distY > 0 ? 'top' : 'bottom', - style, - x, y; - - context.save(); - context.translate(width / 2, height / 2); - context.scale(1, vScale); - - x = Math.cos(bisection) * explode; - y = Math.sin(bisection) * explode; - - // Shadows - if (shadowSize > 0) { - this.plotSlice(x + shadowSize, y + shadowSize, radius, startAngle, endAngle, context); - if (fill) { - context.fillStyle = 'rgba(0,0,0,0.1)'; - context.fill(); - } - } - - this.plotSlice(x, y, radius, startAngle, endAngle, context); - if (fill) { - context.fillStyle = fillStyle; - context.fill(); - } - context.lineWidth = lineWidth; - context.strokeStyle = color; - context.stroke(); - - style = { - size : options.fontSize * 1.2, - color : options.fontColor, - weight : 1.5 - }; - - if (label) { - if (options.htmlText || !options.textEnabled) { - divStyle = 'position:absolute;' + textBaseline + ':' + (height / 2 + (textBaseline === 'top' ? distY : -distY)) + 'px;'; - divStyle += textAlign + ':' + (width / 2 + (textAlign === 'right' ? -distX : distX)) + 'px;'; - html.push('
', label, '
'); - } - else { - style.textAlign = textAlign; - style.textBaseline = textBaseline; - Flotr.drawText(context, label, distX, distY, style); - } - } - - if (options.htmlText || !options.textEnabled) { - var div = Flotr.DOM.node('
'); - Flotr.DOM.insert(div, html.join('')); - Flotr.DOM.insert(options.element, div); - } - - context.restore(); - - // New start angle - this.startAngle = endAngle; - this.slices = this.slices || []; - this.slices.push({ - radius : Math.min(canvas.width, canvas.height) * sizeRatio / 2, - x : x, - y : y, - explode : explode, - start : startAngle, - end : endAngle - }); - }, - plotSlice : function (x, y, radius, startAngle, endAngle, context) { - context.beginPath(); - context.moveTo(x, y); - context.arc(x, y, radius, startAngle, endAngle, false); - context.lineTo(x, y); - context.closePath(); - }, - hit : function (options) { - - var - data = options.data[0], - args = options.args, - index = options.index, - mouse = args[0], - n = args[1], - slice = this.slices[index], - x = mouse.relX - options.width / 2, - y = mouse.relY - options.height / 2, - r = Math.sqrt(x * x + y * y), - theta = Math.atan(y / x), - circle = Math.PI * 2, - explode = slice.explode || options.explode, - start = slice.start % circle, - end = slice.end % circle, - epsilon = options.epsilon; - - if (x < 0) { - theta += Math.PI; - } else if (x > 0 && y < 0) { - theta += circle; - } - - if (r < slice.radius + explode && r > explode) { - if ( - (theta > start && theta < end) || // Normal Slice - (start > end && (theta < end || theta > start)) || // First slice - // TODO: Document the two cases at the end: - (start === end && ((slice.start === slice.end && Math.abs(theta - start) < epsilon) || (slice.start !== slice.end && Math.abs(theta-start) > epsilon))) - ) { - - // TODO Decouple this from hit plugin (chart shouldn't know what n means) - n.x = data[0]; - n.y = data[1]; - n.sAngle = start; - n.eAngle = end; - n.index = 0; - n.seriesIndex = index; - n.fraction = data[1] / this.total; - } - } - }, - drawHit: function (options) { - var - context = options.context, - slice = this.slices[options.args.seriesIndex]; - - context.save(); - context.translate(options.width / 2, options.height / 2); - this.plotSlice(slice.x, slice.y, slice.radius, slice.start, slice.end, context); - context.stroke(); - context.restore(); - }, - clearHit : function (options) { - var - context = options.context, - slice = this.slices[options.args.seriesIndex], - padding = 2 * options.lineWidth, - radius = slice.radius + padding; - - context.save(); - context.translate(options.width / 2, options.height / 2); - context.clearRect( - slice.x - radius, - slice.y - radius, - 2 * radius + padding, - 2 * radius + padding - ); - context.restore(); - }, - extendYRange : function (axis, data) { - this.total = (this.total || 0) + data[0][1]; - } -}); -})(); - -/** Points **/ -Flotr.addType('points', { - options: { - show: false, // => setting to true will show points, false will hide - radius: 3, // => point radius (pixels) - lineWidth: 2, // => line width in pixels - fill: true, // => true to fill the points with a color, false for (transparent) no fill - fillColor: '#FFFFFF', // => fill color. Null to use series color. - fillOpacity: 1, // => opacity of color inside the points - hitRadius: null // => override for points hit radius - }, - - draw : function (options) { - var - context = options.context, - lineWidth = options.lineWidth, - shadowSize = options.shadowSize; - - context.save(); - - if (shadowSize > 0) { - context.lineWidth = shadowSize / 2; - - context.strokeStyle = 'rgba(0,0,0,0.1)'; - this.plot(options, shadowSize / 2 + context.lineWidth / 2); - - context.strokeStyle = 'rgba(0,0,0,0.2)'; - this.plot(options, context.lineWidth / 2); - } - - context.lineWidth = options.lineWidth; - context.strokeStyle = options.color; - if (options.fill) context.fillStyle = options.fillStyle; - - this.plot(options); - context.restore(); - }, - - plot : function (options, offset) { - var - data = options.data, - context = options.context, - xScale = options.xScale, - yScale = options.yScale, - i, x, y; - - for (i = data.length - 1; i > -1; --i) { - y = data[i][1]; - if (y === null) continue; - - x = xScale(data[i][0]); - y = yScale(y); - - if (x < 0 || x > options.width || y < 0 || y > options.height) continue; - - context.beginPath(); - if (offset) { - context.arc(x, y + offset, options.radius, 0, Math.PI, false); - } else { - context.arc(x, y, options.radius, 0, 2 * Math.PI, true); - if (options.fill) context.fill(); - } - context.stroke(); - context.closePath(); - } - } -}); - -/** Radar **/ -Flotr.addType('radar', { - options: { - show: false, // => setting to true will show radar chart, false will hide - lineWidth: 2, // => line width in pixels - fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill - fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill - radiusRatio: 0.90 // => ratio of the radar, against the plot size - }, - draw : function (options) { - var - context = options.context, - shadowSize = options.shadowSize; - - context.save(); - context.translate(options.width / 2, options.height / 2); - context.lineWidth = options.lineWidth; - - // Shadow - context.fillStyle = 'rgba(0,0,0,0.05)'; - context.strokeStyle = 'rgba(0,0,0,0.05)'; - this.plot(options, shadowSize / 2); - context.strokeStyle = 'rgba(0,0,0,0.1)'; - this.plot(options, shadowSize / 4); - - // Chart - context.strokeStyle = options.color; - context.fillStyle = options.fillStyle; - this.plot(options); - - context.restore(); - }, - plot : function (options, offset) { - var - data = options.data, - context = options.context, - radius = Math.min(options.height, options.width) * options.radiusRatio / 2, - step = 2 * Math.PI / data.length, - angle = -Math.PI / 2, - i, ratio; - - offset = offset || 0; - - context.beginPath(); - for (i = 0; i < data.length; ++i) { - ratio = data[i][1] / this.max; - - context[i === 0 ? 'moveTo' : 'lineTo']( - Math.cos(i * step + angle) * radius * ratio + offset, - Math.sin(i * step + angle) * radius * ratio + offset - ); - } - context.closePath(); - if (options.fill) context.fill(); - context.stroke(); - }, - extendYRange : function (axis, data) { - this.max = Math.max(axis.max, this.max || -Number.MAX_VALUE); - } -}); - -Flotr.addType('timeline', { - options: { - show: false, - lineWidth: 1, - barWidth: 0.2, - fill: true, - fillColor: null, - fillOpacity: 0.4, - centered: true - }, - - draw : function (options) { - - var - context = options.context; - - context.save(); - context.lineJoin = 'miter'; - context.lineWidth = options.lineWidth; - context.strokeStyle = options.color; - context.fillStyle = options.fillStyle; - - this.plot(options); - - context.restore(); - }, - - plot : function (options) { - - var - data = options.data, - context = options.context, - xScale = options.xScale, - yScale = options.yScale, - barWidth = options.barWidth, - lineWidth = options.lineWidth, - i; - - Flotr._.each(data, function (timeline) { - - var - x = timeline[0], - y = timeline[1], - w = timeline[2], - h = barWidth, - - xt = Math.ceil(xScale(x)), - wt = Math.ceil(xScale(x + w)) - xt, - yt = Math.round(yScale(y)), - ht = Math.round(yScale(y - h)) - yt, - - x0 = xt - lineWidth / 2, - y0 = Math.round(yt - ht / 2) - lineWidth / 2; - - context.strokeRect(x0, y0, wt, ht); - context.fillRect(x0, y0, wt, ht); - - }); - }, - - extendRange : function (series) { - - var - data = series.data, - xa = series.xaxis, - ya = series.yaxis, - w = series.timeline.barWidth; - - if (xa.options.min === null) - xa.min = xa.datamin - w / 2; - - if (xa.options.max === null) { - - var - max = xa.max; - - Flotr._.each(data, function (timeline) { - max = Math.max(max, timeline[0] + timeline[2]); - }, this); - - xa.max = max + w / 2; - } - - if (ya.options.min === null) - ya.min = ya.datamin - w; - if (ya.options.min === null) - ya.max = ya.datamax + w; - } - -}); - -(function () { - -var D = Flotr.DOM; - -Flotr.addPlugin('crosshair', { - options: { - mode: null, // => one of null, 'x', 'y' or 'xy' - color: '#FF0000', // => crosshair color - hideCursor: true // => hide the cursor when the crosshair is shown - }, - callbacks: { - 'flotr:mousemove': function(e, pos) { - if (this.options.crosshair.mode) { - this.crosshair.clearCrosshair(); - this.crosshair.drawCrosshair(pos); - } - } - }, - /** - * Draws the selection box. - */ - drawCrosshair: function(pos) { - var octx = this.octx, - options = this.options.crosshair, - plotOffset = this.plotOffset, - x = plotOffset.left + Math.round(pos.relX) + .5, - y = plotOffset.top + Math.round(pos.relY) + .5; - - if (pos.relX < 0 || pos.relY < 0 || pos.relX > this.plotWidth || pos.relY > this.plotHeight) { - this.el.style.cursor = null; - D.removeClass(this.el, 'flotr-crosshair'); - return; - } - - if (options.hideCursor) { - this.el.style.cursor = 'none'; - D.addClass(this.el, 'flotr-crosshair'); - } - - octx.save(); - octx.strokeStyle = options.color; - octx.lineWidth = 1; - octx.beginPath(); - - if (options.mode.indexOf('x') != -1) { - octx.moveTo(x, plotOffset.top); - octx.lineTo(x, plotOffset.top + this.plotHeight); - } - - if (options.mode.indexOf('y') != -1) { - octx.moveTo(plotOffset.left, y); - octx.lineTo(plotOffset.left + this.plotWidth, y); - } - - octx.stroke(); - octx.restore(); - }, - /** - * Removes the selection box from the overlay canvas. - */ - clearCrosshair: function() { - - var - plotOffset = this.plotOffset, - position = this.lastMousePos, - context = this.octx; - - if (position) { - context.clearRect( - Math.round(position.relX) + plotOffset.left, - plotOffset.top, - 1, - this.plotHeight + 1 - ); - context.clearRect( - plotOffset.left, - Math.round(position.relY) + plotOffset.top, - this.plotWidth + 1, - 1 - ); - } - } -}); -})(); - -(function() { - -var - D = Flotr.DOM, - _ = Flotr._; - -function getImage (type, canvas, width, height) { - - // TODO add scaling for w / h - var - mime = 'image/'+type, - data = canvas.toDataURL(mime), - image = new Image(); - image.src = data; - return image; -} - -Flotr.addPlugin('download', { - - saveImage: function (type, width, height, replaceCanvas) { - var image = null; - if (Flotr.isIE && Flotr.isIE < 9) { - image = ''+this.canvas.firstChild.innerHTML+''; - return window.open().document.write(image); - } - - if (type !== 'jpeg' && type !== 'png') return; - - image = getImage(type, this.canvas, width, height); - - if (_.isElement(image) && replaceCanvas) { - this.download.restoreCanvas(); - D.hide(this.canvas); - D.hide(this.overlay); - D.setStyles({position: 'absolute'}); - D.insert(this.el, image); - this.saveImageElement = image; - } else { - return window.open(image.src); - } - }, - - restoreCanvas: function() { - D.show(this.canvas); - D.show(this.overlay); - if (this.saveImageElement) this.el.removeChild(this.saveImageElement); - this.saveImageElement = null; - } -}); - -})(); - -(function () { - -var E = Flotr.EventAdapter, - _ = Flotr._; - -Flotr.addPlugin('graphGrid', { - - callbacks: { - 'flotr:beforedraw' : function () { - this.graphGrid.drawGrid(); - }, - 'flotr:afterdraw' : function () { - this.graphGrid.drawOutline(); - } - }, - - drawGrid: function(){ - - var - ctx = this.ctx, - options = this.options, - grid = options.grid, - verticalLines = grid.verticalLines, - horizontalLines = grid.horizontalLines, - minorVerticalLines = grid.minorVerticalLines, - minorHorizontalLines = grid.minorHorizontalLines, - plotHeight = this.plotHeight, - plotWidth = this.plotWidth, - a, v, i, j; - - if(verticalLines || minorVerticalLines || - horizontalLines || minorHorizontalLines){ - E.fire(this.el, 'flotr:beforegrid', [this.axes.x, this.axes.y, options, this]); - } - ctx.save(); - ctx.lineWidth = 1; - ctx.strokeStyle = grid.tickColor; - - function circularHorizontalTicks (ticks) { - for(i = 0; i < ticks.length; ++i){ - var ratio = ticks[i].v / a.max; - for(j = 0; j <= sides; ++j){ - ctx[j === 0 ? 'moveTo' : 'lineTo']( - Math.cos(j*coeff+angle)*radius*ratio, - Math.sin(j*coeff+angle)*radius*ratio - ); - } - } - } - function drawGridLines (ticks, callback) { - _.each(_.pluck(ticks, 'v'), function(v){ - // Don't show lines on upper and lower bounds. - if ((v <= a.min || v >= a.max) || - (v == a.min || v == a.max) && grid.outlineWidth) - return; - callback(Math.floor(a.d2p(v)) + ctx.lineWidth/2); - }); - } - function drawVerticalLines (x) { - ctx.moveTo(x, 0); - ctx.lineTo(x, plotHeight); - } - function drawHorizontalLines (y) { - ctx.moveTo(0, y); - ctx.lineTo(plotWidth, y); - } - - if (grid.circular) { - ctx.translate(this.plotOffset.left+plotWidth/2, this.plotOffset.top+plotHeight/2); - var radius = Math.min(plotHeight, plotWidth)*options.radar.radiusRatio/2, - sides = this.axes.x.ticks.length, - coeff = 2*(Math.PI/sides), - angle = -Math.PI/2; - - // Draw grid lines in vertical direction. - ctx.beginPath(); - - a = this.axes.y; - - if(horizontalLines){ - circularHorizontalTicks(a.ticks); - } - if(minorHorizontalLines){ - circularHorizontalTicks(a.minorTicks); - } - - if(verticalLines){ - _.times(sides, function(i){ - ctx.moveTo(0, 0); - ctx.lineTo(Math.cos(i*coeff+angle)*radius, Math.sin(i*coeff+angle)*radius); - }); - } - ctx.stroke(); - } - else { - ctx.translate(this.plotOffset.left, this.plotOffset.top); - - // Draw grid background, if present in options. - if(grid.backgroundColor){ - ctx.fillStyle = this.processColor(grid.backgroundColor, {x1: 0, y1: 0, x2: plotWidth, y2: plotHeight}); - ctx.fillRect(0, 0, plotWidth, plotHeight); - } - - ctx.beginPath(); - - a = this.axes.x; - if (verticalLines) drawGridLines(a.ticks, drawVerticalLines); - if (minorVerticalLines) drawGridLines(a.minorTicks, drawVerticalLines); - - a = this.axes.y; - if (horizontalLines) drawGridLines(a.ticks, drawHorizontalLines); - if (minorHorizontalLines) drawGridLines(a.minorTicks, drawHorizontalLines); - - ctx.stroke(); - } - - ctx.restore(); - if(verticalLines || minorVerticalLines || - horizontalLines || minorHorizontalLines){ - E.fire(this.el, 'flotr:aftergrid', [this.axes.x, this.axes.y, options, this]); - } - }, - - drawOutline: function(){ - var - that = this, - options = that.options, - grid = options.grid, - outline = grid.outline, - ctx = that.ctx, - backgroundImage = grid.backgroundImage, - plotOffset = that.plotOffset, - leftOffset = plotOffset.left, - topOffset = plotOffset.top, - plotWidth = that.plotWidth, - plotHeight = that.plotHeight, - v, img, src, left, top, globalAlpha; - - if (!grid.outlineWidth) return; - - ctx.save(); - - if (grid.circular) { - ctx.translate(leftOffset + plotWidth / 2, topOffset + plotHeight / 2); - var radius = Math.min(plotHeight, plotWidth) * options.radar.radiusRatio / 2, - sides = this.axes.x.ticks.length, - coeff = 2*(Math.PI/sides), - angle = -Math.PI/2; - - // Draw axis/grid border. - ctx.beginPath(); - ctx.lineWidth = grid.outlineWidth; - ctx.strokeStyle = grid.color; - ctx.lineJoin = 'round'; - - for(i = 0; i <= sides; ++i){ - ctx[i === 0 ? 'moveTo' : 'lineTo'](Math.cos(i*coeff+angle)*radius, Math.sin(i*coeff+angle)*radius); - } - //ctx.arc(0, 0, radius, 0, Math.PI*2, true); - - ctx.stroke(); - } - else { - ctx.translate(leftOffset, topOffset); - - // Draw axis/grid border. - var lw = grid.outlineWidth, - orig = 0.5-lw+((lw+1)%2/2), - lineTo = 'lineTo', - moveTo = 'moveTo'; - ctx.lineWidth = lw; - ctx.strokeStyle = grid.color; - ctx.lineJoin = 'miter'; - ctx.beginPath(); - ctx.moveTo(orig, orig); - plotWidth = plotWidth - (lw / 2) % 1; - plotHeight = plotHeight + lw / 2; - ctx[outline.indexOf('n') !== -1 ? lineTo : moveTo](plotWidth, orig); - ctx[outline.indexOf('e') !== -1 ? lineTo : moveTo](plotWidth, plotHeight); - ctx[outline.indexOf('s') !== -1 ? lineTo : moveTo](orig, plotHeight); - ctx[outline.indexOf('w') !== -1 ? lineTo : moveTo](orig, orig); - ctx.stroke(); - ctx.closePath(); - } - - ctx.restore(); - - if (backgroundImage) { - - src = backgroundImage.src || backgroundImage; - left = (parseInt(backgroundImage.left, 10) || 0) + plotOffset.left; - top = (parseInt(backgroundImage.top, 10) || 0) + plotOffset.top; - img = new Image(); - - img.onload = function() { - ctx.save(); - if (backgroundImage.alpha) ctx.globalAlpha = backgroundImage.alpha; - ctx.globalCompositeOperation = 'destination-over'; - ctx.drawImage(img, 0, 0, img.width, img.height, left, top, plotWidth, plotHeight); - ctx.restore(); - }; - - img.src = src; - } - } -}); - -})(); - -(function () { - -var - D = Flotr.DOM, - _ = Flotr._, - flotr = Flotr, - S_MOUSETRACK = 'opacity:0.7;background-color:#000;color:#fff;display:none;position:absolute;padding:2px 8px;-moz-border-radius:4px;border-radius:4px;white-space:nowrap;'; - -Flotr.addPlugin('hit', { - callbacks: { - 'flotr:mousemove': function(e, pos) { - this.hit.track(pos); - }, - 'flotr:click': function(pos) { - var - hit = this.hit.track(pos); - _.defaults(pos, hit); - }, - 'flotr:mouseout': function() { - this.hit.clearHit(); - }, - 'flotr:destroy': function() { - this.mouseTrack = null; - } - }, - track : function (pos) { - if (this.options.mouse.track || _.any(this.series, function(s){return s.mouse && s.mouse.track;})) { - return this.hit.hit(pos); - } - }, - /** - * Try a method on a graph type. If the method exists, execute it. - * @param {Object} series - * @param {String} method Method name. - * @param {Array} args Arguments applied to method. - * @return executed successfully or failed. - */ - executeOnType: function(s, method, args){ - var - success = false, - options; - - if (!_.isArray(s)) s = [s]; - - function e(s, index) { - _.each(_.keys(flotr.graphTypes), function (type) { - if (s[type] && s[type].show && this[type][method]) { - options = this.getOptions(s, type); - - options.fill = !!s.mouse.fillColor; - options.fillStyle = this.processColor(s.mouse.fillColor || '#ffffff', {opacity: s.mouse.fillOpacity}); - options.color = s.mouse.lineColor; - options.context = this.octx; - options.index = index; - - if (args) options.args = args; - this[type][method].call(this[type], options); - success = true; - } - }, this); - } - _.each(s, e, this); - - return success; - }, - /** - * Updates the mouse tracking point on the overlay. - */ - drawHit: function(n){ - var octx = this.octx, - s = n.series; - - if (s.mouse.lineColor) { - octx.save(); - octx.lineWidth = (s.points ? s.points.lineWidth : 1); - octx.strokeStyle = s.mouse.lineColor; - octx.fillStyle = this.processColor(s.mouse.fillColor || '#ffffff', {opacity: s.mouse.fillOpacity}); - octx.translate(this.plotOffset.left, this.plotOffset.top); - - if (!this.hit.executeOnType(s, 'drawHit', n)) { - var - xa = n.xaxis, - ya = n.yaxis; - - octx.beginPath(); - // TODO fix this (points) should move to general testable graph mixin - octx.arc(xa.d2p(n.x), ya.d2p(n.y), s.points.hitRadius || s.points.radius || s.mouse.radius, 0, 2 * Math.PI, true); - octx.fill(); - octx.stroke(); - octx.closePath(); - } - octx.restore(); - this.clip(octx); - } - this.prevHit = n; - }, - /** - * Removes the mouse tracking point from the overlay. - */ - clearHit: function(){ - var prev = this.prevHit, - octx = this.octx, - plotOffset = this.plotOffset; - octx.save(); - octx.translate(plotOffset.left, plotOffset.top); - if (prev) { - if (!this.hit.executeOnType(prev.series, 'clearHit', this.prevHit)) { - // TODO fix this (points) should move to general testable graph mixin - var - s = prev.series, - lw = (s.points ? s.points.lineWidth : 1); - offset = (s.points.hitRadius || s.points.radius || s.mouse.radius) + lw; - octx.clearRect( - prev.xaxis.d2p(prev.x) - offset, - prev.yaxis.d2p(prev.y) - offset, - offset*2, - offset*2 - ); - } - D.hide(this.mouseTrack); - this.prevHit = null; - } - octx.restore(); - }, - /** - * Retrieves the nearest data point from the mouse cursor. If it's within - * a certain range, draw a point on the overlay canvas and display the x and y - * value of the data. - * @param {Object} mouse - Object that holds the relative x and y coordinates of the cursor. - */ - hit : function (mouse) { - - var - options = this.options, - prevHit = this.prevHit, - closest, sensibility, dataIndex, seriesIndex, series, value, xaxis, yaxis, n; - - if (this.series.length === 0) return; - - // Nearest data element. - // dist, x, y, relX, relY, absX, absY, sAngle, eAngle, fraction, mouse, - // xaxis, yaxis, series, index, seriesIndex - n = { - relX : mouse.relX, - relY : mouse.relY, - absX : mouse.absX, - absY : mouse.absY - }; - - if (options.mouse.trackY && - !options.mouse.trackAll && - this.hit.executeOnType(this.series, 'hit', [mouse, n]) && - !_.isUndefined(n.seriesIndex)) - { - series = this.series[n.seriesIndex]; - n.series = series; - n.mouse = series.mouse; - n.xaxis = series.xaxis; - n.yaxis = series.yaxis; - } else { - - closest = this.hit.closest(mouse); - - if (closest) { - - closest = options.mouse.trackY ? closest.point : closest.x; - seriesIndex = closest.seriesIndex; - series = this.series[seriesIndex]; - xaxis = series.xaxis; - yaxis = series.yaxis; - sensibility = 2 * series.mouse.sensibility; - - if - (options.mouse.trackAll || - (closest.distanceX < sensibility / xaxis.scale && - (!options.mouse.trackY || closest.distanceY < sensibility / yaxis.scale))) - { - n.series = series; - n.xaxis = series.xaxis; - n.yaxis = series.yaxis; - n.mouse = series.mouse; - n.x = closest.x; - n.y = closest.y; - n.dist = closest.distance; - n.index = closest.dataIndex; - n.seriesIndex = seriesIndex; - } - } - } - - if (!prevHit || (prevHit.index !== n.index || prevHit.seriesIndex !== n.seriesIndex)) { - this.hit.clearHit(); - if (n.series && n.mouse && n.mouse.track) { - this.hit.drawMouseTrack(n); - this.hit.drawHit(n); - Flotr.EventAdapter.fire(this.el, 'flotr:hit', [n, this]); - } - } - - return n; - }, - - closest : function (mouse) { - - var - series = this.series, - options = this.options, - relX = mouse.relX, - relY = mouse.relY, - compare = Number.MAX_VALUE, - compareX = Number.MAX_VALUE, - closest = {}, - closestX = {}, - check = false, - serie, data, - distance, distanceX, distanceY, - mouseX, mouseY, - x, y, i, j; - - function setClosest (o) { - o.distance = distance; - o.distanceX = distanceX; - o.distanceY = distanceY; - o.seriesIndex = i; - o.dataIndex = j; - o.x = x; - o.y = y; - check = true; - } - - for (i = 0; i < series.length; i++) { - - serie = series[i]; - data = serie.data; - mouseX = serie.xaxis.p2d(relX); - mouseY = serie.yaxis.p2d(relY); - - for (j = data.length; j--;) { - - x = data[j][0]; - y = data[j][1]; - - if (x === null || y === null) continue; - - // don't check if the point isn't visible in the current range - if (x < serie.xaxis.min || x > serie.xaxis.max) continue; - - distanceX = Math.abs(x - mouseX); - distanceY = Math.abs(y - mouseY); - - // Skip square root for speed - distance = distanceX * distanceX + distanceY * distanceY; - - if (distance < compare) { - compare = distance; - setClosest(closest); - } - - if (distanceX < compareX) { - compareX = distanceX; - setClosest(closestX); - } - } - } - - return check ? { - point : closest, - x : closestX - } : false; - }, - - drawMouseTrack : function (n) { - - var - pos = '', - s = n.series, - p = n.mouse.position, - m = n.mouse.margin, - x = n.x, - y = n.y, - elStyle = S_MOUSETRACK, - mouseTrack = this.mouseTrack, - plotOffset = this.plotOffset, - left = plotOffset.left, - right = plotOffset.right, - bottom = plotOffset.bottom, - top = plotOffset.top, - decimals = n.mouse.trackDecimals, - options = this.options; - - // Create - if (!mouseTrack) { - mouseTrack = D.node('
'); - this.mouseTrack = mouseTrack; - D.insert(this.el, mouseTrack); - } - - if (!n.mouse.relative) { // absolute to the canvas - - if (p.charAt(0) == 'n') pos += 'top:' + (m + top) + 'px;bottom:auto;'; - else if (p.charAt(0) == 's') pos += 'bottom:' + (m + bottom) + 'px;top:auto;'; - if (p.charAt(1) == 'e') pos += 'right:' + (m + right) + 'px;left:auto;'; - else if (p.charAt(1) == 'w') pos += 'left:' + (m + left) + 'px;right:auto;'; - - // Pie - } else if (s.pie && s.pie.show) { - var center = { - x: (this.plotWidth)/2, - y: (this.plotHeight)/2 - }, - radius = (Math.min(this.canvasWidth, this.canvasHeight) * s.pie.sizeRatio) / 2, - bisection = n.sAngle one of null, 'x', 'y' or 'xy' - color: '#B6D9FF', // => selection box color - fps: 20 // => frames-per-second - }, - - callbacks: { - 'flotr:mouseup' : function (event) { - - var - options = this.options.selection, - selection = this.selection, - pointer = this.getEventPosition(event); - - if (!options || !options.mode) return; - if (selection.interval) clearInterval(selection.interval); - - if (this.multitouches) { - selection.updateSelection(); - } else - if (!options.pinchOnly) { - selection.setSelectionPos(selection.selection.second, pointer); - } - selection.clearSelection(); - - if(selection.selecting && selection.selectionIsSane()){ - selection.drawSelection(); - selection.fireSelectEvent(); - this.ignoreClick = true; - } - }, - 'flotr:mousedown' : function (event) { - - var - options = this.options.selection, - selection = this.selection, - pointer = this.getEventPosition(event); - - if (!options || !options.mode) return; - if (!options.mode || (!isLeftClick(event) && _.isUndefined(event.touches))) return; - if (!options.pinchOnly) selection.setSelectionPos(selection.selection.first, pointer); - if (selection.interval) clearInterval(selection.interval); - - this.lastMousePos.pageX = null; - selection.selecting = false; - selection.interval = setInterval( - _.bind(selection.updateSelection, this), - 1000 / options.fps - ); - }, - 'flotr:destroy' : function (event) { - clearInterval(this.selection.interval); - } - }, - - // TODO This isn't used. Maybe it belongs in the draw area and fire select event methods? - getArea: function() { - - var - s = this.selection.selection, - a = this.axes, - first = s.first, - second = s.second, - x1, x2, y1, y2; - - x1 = a.x.p2d(s.first.x); - x2 = a.x.p2d(s.second.x); - y1 = a.y.p2d(s.first.y); - y2 = a.y.p2d(s.second.y); - - return { - x1 : Math.min(x1, x2), - y1 : Math.min(y1, y2), - x2 : Math.max(x1, x2), - y2 : Math.max(y1, y2), - xfirst : x1, - xsecond : x2, - yfirst : y1, - ysecond : y2 - }; - }, - - selection: {first: {x: -1, y: -1}, second: {x: -1, y: -1}}, - prevSelection: null, - interval: null, - - /** - * Fires the 'flotr:select' event when the user made a selection. - */ - fireSelectEvent: function(name){ - var - area = this.selection.getArea(); - name = name || 'select'; - area.selection = this.selection.selection; - E.fire(this.el, 'flotr:'+name, [area, this]); - }, - - /** - * Allows the user the manually select an area. - * @param {Object} area - Object with coordinates to select. - */ - setSelection: function(area, preventEvent){ - var options = this.options, - xa = this.axes.x, - ya = this.axes.y, - vertScale = ya.scale, - hozScale = xa.scale, - selX = options.selection.mode.indexOf('x') != -1, - selY = options.selection.mode.indexOf('y') != -1, - s = this.selection.selection; - - this.selection.clearSelection(); - - s.first.y = boundY((selX && !selY) ? 0 : (ya.max - area.y1) * vertScale, this); - s.second.y = boundY((selX && !selY) ? this.plotHeight - 1: (ya.max - area.y2) * vertScale, this); - s.first.x = boundX((selY && !selX) ? 0 : (area.x1 - xa.min) * hozScale, this); - s.second.x = boundX((selY && !selX) ? this.plotWidth : (area.x2 - xa.min) * hozScale, this); - - this.selection.drawSelection(); - if (!preventEvent) - this.selection.fireSelectEvent(); - }, - - /** - * Calculates the position of the selection. - * @param {Object} pos - Position object. - * @param {Event} event - Event object. - */ - setSelectionPos: function(pos, pointer) { - var mode = this.options.selection.mode, - selection = this.selection.selection; - - if(mode.indexOf('x') == -1) { - pos.x = (pos == selection.first) ? 0 : this.plotWidth; - }else{ - pos.x = boundX(pointer.relX, this); - } - - if (mode.indexOf('y') == -1) { - pos.y = (pos == selection.first) ? 0 : this.plotHeight - 1; - }else{ - pos.y = boundY(pointer.relY, this); - } - }, - /** - * Draws the selection box. - */ - drawSelection: function() { - - this.selection.fireSelectEvent('selecting'); - - var s = this.selection.selection, - octx = this.octx, - options = this.options, - plotOffset = this.plotOffset, - prevSelection = this.selection.prevSelection; - - if (prevSelection && - s.first.x == prevSelection.first.x && - s.first.y == prevSelection.first.y && - s.second.x == prevSelection.second.x && - s.second.y == prevSelection.second.y) { - return; - } - - octx.save(); - octx.strokeStyle = this.processColor(options.selection.color, {opacity: 0.8}); - octx.lineWidth = 1; - octx.lineJoin = 'miter'; - octx.fillStyle = this.processColor(options.selection.color, {opacity: 0.4}); - - this.selection.prevSelection = { - first: { x: s.first.x, y: s.first.y }, - second: { x: s.second.x, y: s.second.y } - }; - - var x = Math.min(s.first.x, s.second.x), - y = Math.min(s.first.y, s.second.y), - w = Math.abs(s.second.x - s.first.x), - h = Math.abs(s.second.y - s.first.y); - - octx.fillRect(x + plotOffset.left+0.5, y + plotOffset.top+0.5, w, h); - octx.strokeRect(x + plotOffset.left+0.5, y + plotOffset.top+0.5, w, h); - octx.restore(); - }, - - /** - * Updates (draws) the selection box. - */ - updateSelection: function(){ - if (!this.lastMousePos.pageX) return; - - this.selection.selecting = true; - - if (this.multitouches) { - this.selection.setSelectionPos(this.selection.selection.first, this.getEventPosition(this.multitouches[0])); - this.selection.setSelectionPos(this.selection.selection.second, this.getEventPosition(this.multitouches[1])); - } else - if (this.options.selection.pinchOnly) { - return; - } else { - this.selection.setSelectionPos(this.selection.selection.second, this.lastMousePos); - } - - this.selection.clearSelection(); - - if(this.selection.selectionIsSane()) { - this.selection.drawSelection(); - } - }, - - /** - * Removes the selection box from the overlay canvas. - */ - clearSelection: function() { - if (!this.selection.prevSelection) return; - - var prevSelection = this.selection.prevSelection, - lw = 1, - plotOffset = this.plotOffset, - x = Math.min(prevSelection.first.x, prevSelection.second.x), - y = Math.min(prevSelection.first.y, prevSelection.second.y), - w = Math.abs(prevSelection.second.x - prevSelection.first.x), - h = Math.abs(prevSelection.second.y - prevSelection.first.y); - - this.octx.clearRect(x + plotOffset.left - lw + 0.5, - y + plotOffset.top - lw, - w + 2 * lw + 0.5, - h + 2 * lw + 0.5); - - this.selection.prevSelection = null; - }, - /** - * Determines whether or not the selection is sane and should be drawn. - * @return {Boolean} - True when sane, false otherwise. - */ - selectionIsSane: function(){ - var s = this.selection.selection; - return Math.abs(s.second.x - s.first.x) >= 5 || - Math.abs(s.second.y - s.first.y) >= 5; - } - -}); - -})(); - -(function () { - -var D = Flotr.DOM; - -Flotr.addPlugin('labels', { - - callbacks : { - 'flotr:afterdraw' : function () { - this.labels.draw(); - } - }, - - draw: function(){ - // Construct fixed width label boxes, which can be styled easily. - var - axis, tick, left, top, xBoxWidth, - radius, sides, coeff, angle, - div, i, html = '', - noLabels = 0, - options = this.options, - ctx = this.ctx, - a = this.axes, - style = { size: options.fontSize }; - - for (i = 0; i < a.x.ticks.length; ++i){ - if (a.x.ticks[i].label) { ++noLabels; } - } - xBoxWidth = this.plotWidth / noLabels; - - if (options.grid.circular) { - ctx.save(); - ctx.translate(this.plotOffset.left + this.plotWidth / 2, - this.plotOffset.top + this.plotHeight / 2); - - radius = this.plotHeight * options.radar.radiusRatio / 2 + options.fontSize; - sides = this.axes.x.ticks.length; - coeff = 2 * (Math.PI / sides); - angle = -Math.PI / 2; - - drawLabelCircular(this, a.x, false); - drawLabelCircular(this, a.x, true); - drawLabelCircular(this, a.y, false); - drawLabelCircular(this, a.y, true); - ctx.restore(); - } - - if (!options.HtmlText && this.textEnabled) { - drawLabelNoHtmlText(this, a.x, 'center', 'top'); - drawLabelNoHtmlText(this, a.x2, 'center', 'bottom'); - drawLabelNoHtmlText(this, a.y, 'right', 'middle'); - drawLabelNoHtmlText(this, a.y2, 'left', 'middle'); - - } else if (( - a.x.options.showLabels || - a.x2.options.showLabels || - a.y.options.showLabels || - a.y2.options.showLabels) && - !options.grid.circular - ) { - - html = ''; - - drawLabelHtml(this, a.x); - drawLabelHtml(this, a.x2); - drawLabelHtml(this, a.y); - drawLabelHtml(this, a.y2); - - ctx.stroke(); - ctx.restore(); - div = D.create('div'); - D.setStyles(div, { - fontSize: 'smaller', - color: options.grid.color - }); - div.className = 'flotr-labels'; - D.insert(this.el, div); - D.insert(div, html); - } - - function drawLabelCircular (graph, axis, minorTicks) { - var - ticks = minorTicks ? axis.minorTicks : axis.ticks, - isX = axis.orientation === 1, - isFirst = axis.n === 1, - style, offset; - - style = { - color : axis.options.color || options.grid.color, - angle : Flotr.toRad(axis.options.labelsAngle), - textBaseline : 'middle' - }; - - for (i = 0; i < ticks.length && - (minorTicks ? axis.options.showMinorLabels : axis.options.showLabels); ++i){ - tick = ticks[i]; - tick.label += ''; - if (!tick.label || !tick.label.length) { continue; } - - x = Math.cos(i * coeff + angle) * radius; - y = Math.sin(i * coeff + angle) * radius; - - style.textAlign = isX ? (Math.abs(x) < 0.1 ? 'center' : (x < 0 ? 'right' : 'left')) : 'left'; - - Flotr.drawText( - ctx, tick.label, - isX ? x : 3, - isX ? y : -(axis.ticks[i].v / axis.max) * (radius - options.fontSize), - style - ); - } - } - - function drawLabelNoHtmlText (graph, axis, textAlign, textBaseline) { - var - isX = axis.orientation === 1, - isFirst = axis.n === 1, - style, offset; - - style = { - color : axis.options.color || options.grid.color, - textAlign : textAlign, - textBaseline : textBaseline, - angle : Flotr.toRad(axis.options.labelsAngle) - }; - style = Flotr.getBestTextAlign(style.angle, style); - - for (i = 0; i < axis.ticks.length && continueShowingLabels(axis); ++i) { - - tick = axis.ticks[i]; - if (!tick.label || !tick.label.length) { continue; } - - offset = axis.d2p(tick.v); - if (offset < 0 || - offset > (isX ? graph.plotWidth : graph.plotHeight)) { continue; } - - Flotr.drawText( - ctx, tick.label, - leftOffset(graph, isX, isFirst, offset), - topOffset(graph, isX, isFirst, offset), - style - ); - - // Only draw on axis y2 - if (!isX && !isFirst) { - ctx.save(); - ctx.strokeStyle = style.color; - ctx.beginPath(); - ctx.moveTo(graph.plotOffset.left + graph.plotWidth - 8, graph.plotOffset.top + axis.d2p(tick.v)); - ctx.lineTo(graph.plotOffset.left + graph.plotWidth, graph.plotOffset.top + axis.d2p(tick.v)); - ctx.stroke(); - ctx.restore(); - } - } - - function continueShowingLabels (axis) { - return axis.options.showLabels && axis.used; - } - function leftOffset (graph, isX, isFirst, offset) { - return graph.plotOffset.left + - (isX ? offset : - (isFirst ? - -options.grid.labelMargin : - options.grid.labelMargin + graph.plotWidth)); - } - function topOffset (graph, isX, isFirst, offset) { - return graph.plotOffset.top + - (isX ? options.grid.labelMargin : offset) + - ((isX && isFirst) ? graph.plotHeight : 0); - } - } - - function drawLabelHtml (graph, axis) { - var - isX = axis.orientation === 1, - isFirst = axis.n === 1, - name = '', - left, style, top, - offset = graph.plotOffset; - - if (!isX && !isFirst) { - ctx.save(); - ctx.strokeStyle = axis.options.color || options.grid.color; - ctx.beginPath(); - } - - if (axis.options.showLabels && (isFirst ? true : axis.used)) { - for (i = 0; i < axis.ticks.length; ++i) { - tick = axis.ticks[i]; - if (!tick.label || !tick.label.length || - ((isX ? offset.left : offset.top) + axis.d2p(tick.v) < 0) || - ((isX ? offset.left : offset.top) + axis.d2p(tick.v) > (isX ? graph.canvasWidth : graph.canvasHeight))) { - continue; - } - top = offset.top + - (isX ? - ((isFirst ? 1 : -1 ) * (graph.plotHeight + options.grid.labelMargin)) : - axis.d2p(tick.v) - axis.maxLabel.height / 2); - left = isX ? (offset.left + axis.d2p(tick.v) - xBoxWidth / 2) : 0; - - name = ''; - if (i === 0) { - name = ' first'; - } else if (i === axis.ticks.length - 1) { - name = ' last'; - } - name += isX ? ' flotr-grid-label-x' : ' flotr-grid-label-y'; - - html += [ - '
' + tick.label + '
' - ].join(' '); - - if (!isX && !isFirst) { - ctx.moveTo(offset.left + graph.plotWidth - 8, offset.top + axis.d2p(tick.v)); - ctx.lineTo(offset.left + graph.plotWidth, offset.top + axis.d2p(tick.v)); - } - } - } - } - } - -}); -})(); - -(function () { - -var - D = Flotr.DOM, - _ = Flotr._; - -Flotr.addPlugin('legend', { - options: { - show: true, // => setting to true will show the legend, hide otherwise - noColumns: 1, // => number of colums in legend table // @todo: doesn't work for HtmlText = false - labelFormatter: function(v){return v;}, // => fn: string -> string - labelBoxBorderColor: '#CCCCCC', // => border color for the little label boxes - labelBoxWidth: 14, - labelBoxHeight: 10, - labelBoxMargin: 5, - container: null, // => container (as jQuery object) to put legend in, null means default on top of graph - position: 'nw', // => position of default legend container within plot - margin: 5, // => distance from grid edge to default legend container within plot - backgroundColor: '#F0F0F0', // => Legend background color. - backgroundOpacity: 0.85// => set to 0 to avoid background, set to 1 for a solid background - }, - callbacks: { - 'flotr:afterinit': function() { - this.legend.insertLegend(); - } - }, - /** - * Adds a legend div to the canvas container or draws it on the canvas. - */ - insertLegend: function(){ - - if(!this.options.legend.show) - return; - - var series = this.series, - plotOffset = this.plotOffset, - options = this.options, - legend = options.legend, - fragments = [], - rowStarted = false, - ctx = this.ctx, - itemCount = _.filter(series, function(s) {return (s.label && !s.hide);}).length, - p = legend.position, - m = legend.margin, - opacity = legend.backgroundOpacity, - i, label, color; - - if (itemCount) { - - var lbw = legend.labelBoxWidth, - lbh = legend.labelBoxHeight, - lbm = legend.labelBoxMargin, - offsetX = plotOffset.left + m, - offsetY = plotOffset.top + m, - labelMaxWidth = 0, - style = { - size: options.fontSize*1.1, - color: options.grid.color - }; - - // We calculate the labels' max width - for(i = series.length - 1; i > -1; --i){ - if(!series[i].label || series[i].hide) continue; - label = legend.labelFormatter(series[i].label); - labelMaxWidth = Math.max(labelMaxWidth, this._text.measureText(label, style).width); - } - - var legendWidth = Math.round(lbw + lbm*3 + labelMaxWidth), - legendHeight = Math.round(itemCount*(lbm+lbh) + lbm); - - // Default Opacity - if (!opacity && !opacity === 0) { - opacity = 0.1; - } - - if (!options.HtmlText && this.textEnabled && !legend.container) { - - if(p.charAt(0) == 's') offsetY = plotOffset.top + this.plotHeight - (m + legendHeight); - if(p.charAt(0) == 'c') offsetY = plotOffset.top + (this.plotHeight/2) - (m + (legendHeight/2)); - if(p.charAt(1) == 'e') offsetX = plotOffset.left + this.plotWidth - (m + legendWidth); - - // Legend box - color = this.processColor(legend.backgroundColor, { opacity : opacity }); - - ctx.fillStyle = color; - ctx.fillRect(offsetX, offsetY, legendWidth, legendHeight); - ctx.strokeStyle = legend.labelBoxBorderColor; - ctx.strokeRect(Flotr.toPixel(offsetX), Flotr.toPixel(offsetY), legendWidth, legendHeight); - - // Legend labels - var x = offsetX + lbm; - var y = offsetY + lbm; - for(i = 0; i < series.length; i++){ - if(!series[i].label || series[i].hide) continue; - label = legend.labelFormatter(series[i].label); - - ctx.fillStyle = series[i].color; - ctx.fillRect(x, y, lbw-1, lbh-1); - - ctx.strokeStyle = legend.labelBoxBorderColor; - ctx.lineWidth = 1; - ctx.strokeRect(Math.ceil(x)-1.5, Math.ceil(y)-1.5, lbw+2, lbh+2); - - // Legend text - Flotr.drawText(ctx, label, x + lbw + lbm, y + lbh, style); - - y += lbh + lbm; - } - } - else { - for(i = 0; i < series.length; ++i){ - if(!series[i].label || series[i].hide) continue; - - if(i % legend.noColumns === 0){ - fragments.push(rowStarted ? '' : ''); - rowStarted = true; - } - - var s = series[i], - boxWidth = legend.labelBoxWidth, - boxHeight = legend.labelBoxHeight; - - label = legend.labelFormatter(s.label); - color = 'background-color:' + ((s.bars && s.bars.show && s.bars.fillColor && s.bars.fill) ? s.bars.fillColor : s.color) + ';'; - - fragments.push( - '', - '
', - '
', // Border - '
', // Background - '
', - '
', - '', - '', label, '' - ); - } - if(rowStarted) fragments.push(''); - - if(fragments.length > 0){ - var table = '' + fragments.join('') + '
'; - if(legend.container){ - D.empty(legend.container); - D.insert(legend.container, table); - } - else { - var styles = {position: 'absolute', 'zIndex': '2', 'border' : '1px solid ' + legend.labelBoxBorderColor}; - - if(p.charAt(0) == 'n') { styles.top = (m + plotOffset.top) + 'px'; styles.bottom = 'auto'; } - else if(p.charAt(0) == 'c') { styles.top = (m + (this.plotHeight - legendHeight) / 2) + 'px'; styles.bottom = 'auto'; } - else if(p.charAt(0) == 's') { styles.bottom = (m + plotOffset.bottom) + 'px'; styles.top = 'auto'; } - if(p.charAt(1) == 'e') { styles.right = (m + plotOffset.right) + 'px'; styles.left = 'auto'; } - else if(p.charAt(1) == 'w') { styles.left = (m + plotOffset.left) + 'px'; styles.right = 'auto'; } - - var div = D.create('div'), size; - div.className = 'flotr-legend'; - D.setStyles(div, styles); - D.insert(div, table); - D.insert(this.el, div); - - if (!opacity) return; - - var c = legend.backgroundColor || options.grid.backgroundColor || '#ffffff'; - - _.extend(styles, D.size(div), { - 'backgroundColor': c, - 'zIndex' : '', - 'border' : '' - }); - styles.width += 'px'; - styles.height += 'px'; - - // Put in the transparent background separately to avoid blended labels and - div = D.create('div'); - div.className = 'flotr-legend-bg'; - D.setStyles(div, styles); - D.opacity(div, opacity); - D.insert(div, ' '); - D.insert(this.el, div); - } - } - } - } - } -}); -})(); - -/** Spreadsheet **/ -(function() { - -function getRowLabel(value){ - if (this.options.spreadsheet.tickFormatter){ - //TODO maybe pass the xaxis formatter to the custom tick formatter as an opt-out? - return this.options.spreadsheet.tickFormatter(value); - } - else { - var t = _.find(this.axes.x.ticks, function(t){return t.v == value;}); - if (t) { - return t.label; - } - return value; - } -} - -var - D = Flotr.DOM, - _ = Flotr._; - -Flotr.addPlugin('spreadsheet', { - options: { - show: false, // => show the data grid using two tabs - tabGraphLabel: 'Graph', - tabDataLabel: 'Data', - toolbarDownload: 'Download CSV', // @todo: add better language support - toolbarSelectAll: 'Select all', - csvFileSeparator: ',', - decimalSeparator: '.', - tickFormatter: null, - initialTab: 'graph' - }, - /** - * Builds the tabs in the DOM - */ - callbacks: { - 'flotr:afterconstruct': function(){ - // @TODO necessary? - //this.el.select('.flotr-tabs-group,.flotr-datagrid-container').invoke('remove'); - - if (!this.options.spreadsheet.show) return; - - var ss = this.spreadsheet, - container = D.node('
'), - graph = D.node('
'+this.options.spreadsheet.tabGraphLabel+'
'), - data = D.node('
'+this.options.spreadsheet.tabDataLabel+'
'), - offset; - - ss.tabsContainer = container; - ss.tabs = { graph : graph, data : data }; - - D.insert(container, graph); - D.insert(container, data); - D.insert(this.el, container); - - offset = D.size(data).height + 2; - this.plotOffset.bottom += offset; - - D.setStyles(container, {top: this.canvasHeight-offset+'px'}); - - this. - observe(graph, 'click', function(){ss.showTab('graph');}). - observe(data, 'click', function(){ss.showTab('data');}); - if (this.options.spreadsheet.initialTab !== 'graph'){ - ss.showTab(this.options.spreadsheet.initialTab); - } - } - }, - /** - * Builds a matrix of the data to make the correspondance between the x values and the y values : - * X value => Y values from the axes - * @return {Array} The data grid - */ - loadDataGrid: function(){ - if (this.seriesData) return this.seriesData; - - var s = this.series, - rows = {}; - - /* The data grid is a 2 dimensions array. There is a row for each X value. - * Each row contains the x value and the corresponding y value for each serie ('undefined' if there isn't one) - **/ - _.each(s, function(serie, i){ - _.each(serie.data, function (v) { - var x = v[0], - y = v[1], - r = rows[x]; - if (r) { - r[i+1] = y; - } else { - var newRow = []; - newRow[0] = x; - newRow[i+1] = y; - rows[x] = newRow; - } - }); - }); - - // The data grid is sorted by x value - this.seriesData = _.sortBy(rows, function(row, x){ - return parseInt(x, 10); - }); - return this.seriesData; - }, - /** - * Constructs the data table for the spreadsheet - * @todo make a spreadsheet manager (Flotr.Spreadsheet) - * @return {Element} The resulting table element - */ - constructDataGrid: function(){ - // If the data grid has already been built, nothing to do here - if (this.spreadsheet.datagrid) return this.spreadsheet.datagrid; - - var s = this.series, - datagrid = this.spreadsheet.loadDataGrid(), - colgroup = [''], - buttonDownload, buttonSelect, t; - - // First row : series' labels - var html = ['']; - html.push(''); - _.each(s, function(serie,i){ - html.push(''); - colgroup.push(''); - }); - html.push(''); - // Data rows - _.each(datagrid, function(row){ - html.push(''); - _.times(s.length+1, function(i){ - var tag = 'td', - value = row[i], - // TODO: do we really want to handle problems with floating point - // precision here? - content = (!_.isUndefined(value) ? Math.round(value*100000)/100000 : ''); - if (i === 0) { - tag = 'th'; - var label = getRowLabel.call(this, content); - if (label) content = label; - } - - html.push('<'+tag+(tag=='th'?' scope="row"':'')+'>'+content+''); - }, this); - html.push(''); - }, this); - colgroup.push(''); - t = D.node(html.join('')); - - /** - * @TODO disabled this - if (!Flotr.isIE || Flotr.isIE == 9) { - function handleMouseout(){ - t.select('colgroup col.hover, th.hover').invoke('removeClassName', 'hover'); - } - function handleMouseover(e){ - var td = e.element(), - siblings = td.previousSiblings(); - t.select('th[scope=col]')[siblings.length-1].addClassName('hover'); - t.select('colgroup col')[siblings.length].addClassName('hover'); - } - _.each(t.select('td'), function(td) { - Flotr.EventAdapter. - observe(td, 'mouseover', handleMouseover). - observe(td, 'mouseout', handleMouseout); - }); - } - */ - - buttonDownload = D.node( - ''); - - buttonSelect = D.node( - ''); - - this. - observe(buttonDownload, 'click', _.bind(this.spreadsheet.downloadCSV, this)). - observe(buttonSelect, 'click', _.bind(this.spreadsheet.selectAllData, this)); - - var toolbar = D.node('
'); - D.insert(toolbar, buttonDownload); - D.insert(toolbar, buttonSelect); - - var containerHeight =this.canvasHeight - D.size(this.spreadsheet.tabsContainer).height-2, - container = D.node('
'); - - D.insert(container, toolbar); - D.insert(container, t); - D.insert(this.el, container); - this.spreadsheet.datagrid = t; - this.spreadsheet.container = container; - - return t; - }, - /** - * Shows the specified tab, by its name - * @todo make a tab manager (Flotr.Tabs) - * @param {String} tabName - The tab name - */ - showTab: function(tabName){ - if (this.spreadsheet.activeTab === tabName){ - return; - } - switch(tabName) { - case 'graph': - D.hide(this.spreadsheet.container); - D.removeClass(this.spreadsheet.tabs.data, 'selected'); - D.addClass(this.spreadsheet.tabs.graph, 'selected'); - break; - case 'data': - if (!this.spreadsheet.datagrid) - this.spreadsheet.constructDataGrid(); - D.show(this.spreadsheet.container); - D.addClass(this.spreadsheet.tabs.data, 'selected'); - D.removeClass(this.spreadsheet.tabs.graph, 'selected'); - break; - default: - throw 'Illegal tab name: ' + tabName; - } - this.spreadsheet.activeTab = tabName; - }, - /** - * Selects the data table in the DOM for copy/paste - */ - selectAllData: function(){ - if (this.spreadsheet.tabs) { - var selection, range, doc, win, node = this.spreadsheet.constructDataGrid(); - - this.spreadsheet.showTab('data'); - - // deferred to be able to select the table - setTimeout(function () { - if ((doc = node.ownerDocument) && (win = doc.defaultView) && - win.getSelection && doc.createRange && - (selection = window.getSelection()) && - selection.removeAllRanges) { - range = doc.createRange(); - range.selectNode(node); - selection.removeAllRanges(); - selection.addRange(range); - } - else if (document.body && document.body.createTextRange && - (range = document.body.createTextRange())) { - range.moveToElementText(node); - range.select(); - } - }, 0); - return true; - } - else return false; - }, - /** - * Converts the data into CSV in order to download a file - */ - downloadCSV: function(){ - var csv = '', - series = this.series, - options = this.options, - dg = this.spreadsheet.loadDataGrid(), - separator = encodeURIComponent(options.spreadsheet.csvFileSeparator); - - if (options.spreadsheet.decimalSeparator === options.spreadsheet.csvFileSeparator) { - throw "The decimal separator is the same as the column separator ("+options.spreadsheet.decimalSeparator+")"; - } - - // The first row - _.each(series, function(serie, i){ - csv += separator+'"'+(serie.label || String.fromCharCode(65+i)).replace(/\"/g, '\\"')+'"'; - }); - - csv += "%0D%0A"; // \r\n - - // For each row - csv += _.reduce(dg, function(memo, row){ - var rowLabel = getRowLabel.call(this, row[0]) || ''; - rowLabel = '"'+(rowLabel+'').replace(/\"/g, '\\"')+'"'; - var numbers = row.slice(1).join(separator); - if (options.spreadsheet.decimalSeparator !== '.') { - numbers = numbers.replace(/\./g, options.spreadsheet.decimalSeparator); - } - return memo + rowLabel+separator+numbers+"%0D%0A"; // \t and \r\n - }, '', this); - - if (Flotr.isIE && Flotr.isIE < 9) { - csv = csv.replace(new RegExp(separator, 'g'), decodeURIComponent(separator)).replace(/%0A/g, '\n').replace(/%0D/g, '\r'); - window.open().document.write(csv); - } - else window.open('data:text/csv,'+csv); - } -}); -})(); - -(function () { - -var D = Flotr.DOM; - -Flotr.addPlugin('titles', { - callbacks: { - 'flotr:afterdraw': function() { - this.titles.drawTitles(); - } - }, - /** - * Draws the title and the subtitle - */ - drawTitles : function () { - var html, - options = this.options, - margin = options.grid.labelMargin, - ctx = this.ctx, - a = this.axes; - - if (!options.HtmlText && this.textEnabled) { - var style = { - size: options.fontSize, - color: options.grid.color, - textAlign: 'center' - }; - - // Add subtitle - if (options.subtitle){ - Flotr.drawText( - ctx, options.subtitle, - this.plotOffset.left + this.plotWidth/2, - this.titleHeight + this.subtitleHeight - 2, - style - ); - } - - style.weight = 1.5; - style.size *= 1.5; - - // Add title - if (options.title){ - Flotr.drawText( - ctx, options.title, - this.plotOffset.left + this.plotWidth/2, - this.titleHeight - 2, - style - ); - } - - style.weight = 1.8; - style.size *= 0.8; - - // Add x axis title - if (a.x.options.title && a.x.used){ - style.textAlign = a.x.options.titleAlign || 'center'; - style.textBaseline = 'top'; - style.angle = Flotr.toRad(a.x.options.titleAngle); - style = Flotr.getBestTextAlign(style.angle, style); - Flotr.drawText( - ctx, a.x.options.title, - this.plotOffset.left + this.plotWidth/2, - this.plotOffset.top + a.x.maxLabel.height + this.plotHeight + 2 * margin, - style - ); - } - - // Add x2 axis title - if (a.x2.options.title && a.x2.used){ - style.textAlign = a.x2.options.titleAlign || 'center'; - style.textBaseline = 'bottom'; - style.angle = Flotr.toRad(a.x2.options.titleAngle); - style = Flotr.getBestTextAlign(style.angle, style); - Flotr.drawText( - ctx, a.x2.options.title, - this.plotOffset.left + this.plotWidth/2, - this.plotOffset.top - a.x2.maxLabel.height - 2 * margin, - style - ); - } - - // Add y axis title - if (a.y.options.title && a.y.used){ - style.textAlign = a.y.options.titleAlign || 'right'; - style.textBaseline = 'middle'; - style.angle = Flotr.toRad(a.y.options.titleAngle); - style = Flotr.getBestTextAlign(style.angle, style); - Flotr.drawText( - ctx, a.y.options.title, - this.plotOffset.left - a.y.maxLabel.width - 2 * margin, - this.plotOffset.top + this.plotHeight / 2, - style - ); - } - - // Add y2 axis title - if (a.y2.options.title && a.y2.used){ - style.textAlign = a.y2.options.titleAlign || 'left'; - style.textBaseline = 'middle'; - style.angle = Flotr.toRad(a.y2.options.titleAngle); - style = Flotr.getBestTextAlign(style.angle, style); - Flotr.drawText( - ctx, a.y2.options.title, - this.plotOffset.left + this.plotWidth + a.y2.maxLabel.width + 2 * margin, - this.plotOffset.top + this.plotHeight / 2, - style - ); - } - } - else { - html = []; - - // Add title - if (options.title) - html.push( - '
', options.title, '
' - ); - - // Add subtitle - if (options.subtitle) - html.push( - '
', options.subtitle, '
' - ); - - html.push(''); - - html.push('
'); - - // Add x axis title - if (a.x.options.title && a.x.used) - html.push( - '
', a.x.options.title, '
' - ); - - // Add x2 axis title - if (a.x2.options.title && a.x2.used) - html.push( - '
', a.x2.options.title, '
' - ); - - // Add y axis title - if (a.y.options.title && a.y.used) - html.push( - '
', a.y.options.title, '
' - ); - - // Add y2 axis title - if (a.y2.options.title && a.y2.used) - html.push( - '
', a.y2.options.title, '
' - ); - - html = html.join(''); - - var div = D.create('div'); - D.setStyles({ - color: options.grid.color - }); - div.className = 'flotr-titles'; - D.insert(this.el, div); - D.insert(div, html); - } - } -}); -})(); +/*! + * bean.js - copyright Jacob Thornton 2011 + * https://github.com/fat/bean + * MIT License + * special thanks to: + * dean edwards: http://dean.edwards.name/ + * dperini: https://github.com/dperini/nwevents + * the entire mootools team: github.com/mootools/mootools-core + */ +/*global module:true, define:true*/ +!function (name, context, definition) { + if (typeof module !== 'undefined') module.exports = definition(name, context); + else if (typeof define === 'function' && typeof define.amd === 'object') define(definition); + else context[name] = definition(name, context); +}('bean', this, function (name, context) { + var win = window + , old = context[name] + , overOut = /over|out/ + , namespaceRegex = /[^\.]*(?=\..*)\.|.*/ + , nameRegex = /\..*/ + , addEvent = 'addEventListener' + , attachEvent = 'attachEvent' + , removeEvent = 'removeEventListener' + , detachEvent = 'detachEvent' + , doc = document || {} + , root = doc.documentElement || {} + , W3C_MODEL = root[addEvent] + , eventSupport = W3C_MODEL ? addEvent : attachEvent + , slice = Array.prototype.slice + , mouseTypeRegex = /click|mouse|menu|drag|drop/i + , touchTypeRegex = /^touch|^gesture/i + , ONE = { one: 1 } // singleton for quick matching making add() do one() + + , nativeEvents = (function (hash, events, i) { + for (i = 0; i < events.length; i++) + hash[events[i]] = 1 + return hash + })({}, ( + 'click dblclick mouseup mousedown contextmenu ' + // mouse buttons + 'mousewheel DOMMouseScroll ' + // mouse wheel + 'mouseover mouseout mousemove selectstart selectend ' + // mouse movement + 'keydown keypress keyup ' + // keyboard + 'orientationchange ' + // mobile + 'focus blur change reset select submit ' + // form elements + 'load unload beforeunload resize move DOMContentLoaded readystatechange ' + // window + 'error abort scroll ' + // misc + (W3C_MODEL ? // element.fireEvent('onXYZ'... is not forgiving if we try to fire an event + // that doesn't actually exist, so make sure we only do these on newer browsers + 'show ' + // mouse buttons + 'input invalid ' + // form elements + 'touchstart touchmove touchend touchcancel ' + // touch + 'gesturestart gesturechange gestureend ' + // gesture + 'message readystatechange pageshow pagehide popstate ' + // window + 'hashchange offline online ' + // window + 'afterprint beforeprint ' + // printing + 'dragstart dragenter dragover dragleave drag drop dragend ' + // dnd + 'loadstart progress suspend emptied stalled loadmetadata ' + // media + 'loadeddata canplay canplaythrough playing waiting seeking ' + // media + 'seeked ended durationchange timeupdate play pause ratechange ' + // media + 'volumechange cuechange ' + // media + 'checking noupdate downloading cached updateready obsolete ' + // appcache + '' : '') + ).split(' ') + ) + + , customEvents = (function () { + function isDescendant(parent, node) { + while ((node = node.parentNode) !== null) { + if (node === parent) return true + } + return false + } + + function check(event) { + var related = event.relatedTarget + if (!related) return related === null + return (related !== this && related.prefix !== 'xul' && !/document/.test(this.toString()) && !isDescendant(this, related)) + } + + return { + mouseenter: { base: 'mouseover', condition: check } + , mouseleave: { base: 'mouseout', condition: check } + , mousewheel: { base: /Firefox/.test(navigator.userAgent) ? 'DOMMouseScroll' : 'mousewheel' } + } + })() + + , fixEvent = (function () { + var commonProps = 'altKey attrChange attrName bubbles cancelable ctrlKey currentTarget detail eventPhase getModifierState isTrusted metaKey relatedNode relatedTarget shiftKey srcElement target timeStamp type view which'.split(' ') + , mouseProps = commonProps.concat('button buttons clientX clientY dataTransfer fromElement offsetX offsetY pageX pageY screenX screenY toElement'.split(' ')) + , keyProps = commonProps.concat('char charCode key keyCode'.split(' ')) + , touchProps = commonProps.concat('touches targetTouches changedTouches scale rotation'.split(' ')) + , preventDefault = 'preventDefault' + , createPreventDefault = function (event) { + return function () { + if (event[preventDefault]) + event[preventDefault]() + else + event.returnValue = false + } + } + , stopPropagation = 'stopPropagation' + , createStopPropagation = function (event) { + return function () { + if (event[stopPropagation]) + event[stopPropagation]() + else + event.cancelBubble = true + } + } + , createStop = function (synEvent) { + return function () { + synEvent[preventDefault]() + synEvent[stopPropagation]() + synEvent.stopped = true + } + } + , copyProps = function (event, result, props) { + var i, p + for (i = props.length; i--;) { + p = props[i] + if (!(p in result) && p in event) result[p] = event[p] + } + } + + return function (event, isNative) { + var result = { originalEvent: event, isNative: isNative } + if (!event) + return result + + var props + , type = event.type + , target = event.target || event.srcElement + + result[preventDefault] = createPreventDefault(event) + result[stopPropagation] = createStopPropagation(event) + result.stop = createStop(result) + result.target = target && target.nodeType === 3 ? target.parentNode : target + + if (isNative) { // we only need basic augmentation on custom events, the rest is too expensive + if (type.indexOf('key') !== -1) { + props = keyProps + result.keyCode = event.which || event.keyCode + } else if (mouseTypeRegex.test(type)) { + props = mouseProps + result.rightClick = event.which === 3 || event.button === 2 + result.pos = { x: 0, y: 0 } + if (event.pageX || event.pageY) { + result.clientX = event.pageX + result.clientY = event.pageY + } else if (event.clientX || event.clientY) { + result.clientX = event.clientX + doc.body.scrollLeft + root.scrollLeft + result.clientY = event.clientY + doc.body.scrollTop + root.scrollTop + } + if (overOut.test(type)) + result.relatedTarget = event.relatedTarget || event[(type === 'mouseover' ? 'from' : 'to') + 'Element'] + } else if (touchTypeRegex.test(type)) { + props = touchProps + } + copyProps(event, result, props || commonProps) + } + return result + } + })() + + // if we're in old IE we can't do onpropertychange on doc or win so we use doc.documentElement for both + , targetElement = function (element, isNative) { + return !W3C_MODEL && !isNative && (element === doc || element === win) ? root : element + } + + // we use one of these per listener, of any type + , RegEntry = (function () { + function entry(element, type, handler, original, namespaces) { + this.element = element + this.type = type + this.handler = handler + this.original = original + this.namespaces = namespaces + this.custom = customEvents[type] + this.isNative = nativeEvents[type] && element[eventSupport] + this.eventType = W3C_MODEL || this.isNative ? type : 'propertychange' + this.customType = !W3C_MODEL && !this.isNative && type + this.target = targetElement(element, this.isNative) + this.eventSupport = this.target[eventSupport] + } + + entry.prototype = { + // given a list of namespaces, is our entry in any of them? + inNamespaces: function (checkNamespaces) { + var i, j + if (!checkNamespaces) + return true + if (!this.namespaces) + return false + for (i = checkNamespaces.length; i--;) { + for (j = this.namespaces.length; j--;) { + if (checkNamespaces[i] === this.namespaces[j]) + return true + } + } + return false + } + + // match by element, original fn (opt), handler fn (opt) + , matches: function (checkElement, checkOriginal, checkHandler) { + return this.element === checkElement && + (!checkOriginal || this.original === checkOriginal) && + (!checkHandler || this.handler === checkHandler) + } + } + + return entry + })() + + , registry = (function () { + // our map stores arrays by event type, just because it's better than storing + // everything in a single array. uses '$' as a prefix for the keys for safety + var map = {} + + // generic functional search of our registry for matching listeners, + // `fn` returns false to break out of the loop + , forAll = function (element, type, original, handler, fn) { + if (!type || type === '*') { + // search the whole registry + for (var t in map) { + if (t.charAt(0) === '$') + forAll(element, t.substr(1), original, handler, fn) + } + } else { + var i = 0, l, list = map['$' + type], all = element === '*' + if (!list) + return + for (l = list.length; i < l; i++) { + if (all || list[i].matches(element, original, handler)) + if (!fn(list[i], list, i, type)) + return + } + } + } + + , has = function (element, type, original) { + // we're not using forAll here simply because it's a bit slower and this + // needs to be fast + var i, list = map['$' + type] + if (list) { + for (i = list.length; i--;) { + if (list[i].matches(element, original, null)) + return true + } + } + return false + } + + , get = function (element, type, original) { + var entries = [] + forAll(element, type, original, null, function (entry) { return entries.push(entry) }) + return entries + } + + , put = function (entry) { + (map['$' + entry.type] || (map['$' + entry.type] = [])).push(entry) + return entry + } + + , del = function (entry) { + forAll(entry.element, entry.type, null, entry.handler, function (entry, list, i) { + list.splice(i, 1) + if (list.length === 0) + delete map['$' + entry.type] + return false + }) + } + + // dump all entries, used for onunload + , entries = function () { + var t, entries = [] + for (t in map) { + if (t.charAt(0) === '$') + entries = entries.concat(map[t]) + } + return entries + } + + return { has: has, get: get, put: put, del: del, entries: entries } + })() + + // add and remove listeners to DOM elements + , listener = W3C_MODEL ? function (element, type, fn, add) { + element[add ? addEvent : removeEvent](type, fn, false) + } : function (element, type, fn, add, custom) { + if (custom && add && element['_on' + custom] === null) + element['_on' + custom] = 0 + element[add ? attachEvent : detachEvent]('on' + type, fn) + } + + , nativeHandler = function (element, fn, args) { + return function (event) { + event = fixEvent(event || ((this.ownerDocument || this.document || this).parentWindow || win).event, true) + return fn.apply(element, [event].concat(args)) + } + } + + , customHandler = function (element, fn, type, condition, args, isNative) { + return function (event) { + if (condition ? condition.apply(this, arguments) : W3C_MODEL ? true : event && event.propertyName === '_on' + type || !event) { + if (event) + event = fixEvent(event || ((this.ownerDocument || this.document || this).parentWindow || win).event, isNative) + fn.apply(element, event && (!args || args.length === 0) ? arguments : slice.call(arguments, event ? 0 : 1).concat(args)) + } + } + } + + , once = function (rm, element, type, fn, originalFn) { + // wrap the handler in a handler that does a remove as well + return function () { + rm(element, type, originalFn) + fn.apply(this, arguments) + } + } + + , removeListener = function (element, orgType, handler, namespaces) { + var i, l, entry + , type = (orgType && orgType.replace(nameRegex, '')) + , handlers = registry.get(element, type, handler) + + for (i = 0, l = handlers.length; i < l; i++) { + if (handlers[i].inNamespaces(namespaces)) { + if ((entry = handlers[i]).eventSupport) + listener(entry.target, entry.eventType, entry.handler, false, entry.type) + // TODO: this is problematic, we have a registry.get() and registry.del() that + // both do registry searches so we waste cycles doing this. Needs to be rolled into + // a single registry.forAll(fn) that removes while finding, but the catch is that + // we'll be splicing the arrays that we're iterating over. Needs extra tests to + // make sure we don't screw it up. @rvagg + registry.del(entry) + } + } + } + + , addListener = function (element, orgType, fn, originalFn, args) { + var entry + , type = orgType.replace(nameRegex, '') + , namespaces = orgType.replace(namespaceRegex, '').split('.') + + if (registry.has(element, type, fn)) + return element // no dupe + if (type === 'unload') + fn = once(removeListener, element, type, fn, originalFn) // self clean-up + if (customEvents[type]) { + if (customEvents[type].condition) + fn = customHandler(element, fn, type, customEvents[type].condition, true) + type = customEvents[type].base || type + } + entry = registry.put(new RegEntry(element, type, fn, originalFn, namespaces[0] && namespaces)) + entry.handler = entry.isNative ? + nativeHandler(element, entry.handler, args) : + customHandler(element, entry.handler, type, false, args, false) + if (entry.eventSupport) + listener(entry.target, entry.eventType, entry.handler, true, entry.customType) + } + + , del = function (selector, fn, $) { + return function (e) { + var target, i, array = typeof selector === 'string' ? $(selector, this) : selector + for (target = e.target; target && target !== this; target = target.parentNode) { + for (i = array.length; i--;) { + if (array[i] === target) { + return fn.apply(target, arguments) + } + } + } + } + } + + , remove = function (element, typeSpec, fn) { + var k, m, type, namespaces, i + , rm = removeListener + , isString = typeSpec && typeof typeSpec === 'string' + + if (isString && typeSpec.indexOf(' ') > 0) { + // remove(el, 't1 t2 t3', fn) or remove(el, 't1 t2 t3') + typeSpec = typeSpec.split(' ') + for (i = typeSpec.length; i--;) + remove(element, typeSpec[i], fn) + return element + } + type = isString && typeSpec.replace(nameRegex, '') + if (type && customEvents[type]) + type = customEvents[type].type + if (!typeSpec || isString) { + // remove(el) or remove(el, t1.ns) or remove(el, .ns) or remove(el, .ns1.ns2.ns3) + if (namespaces = isString && typeSpec.replace(namespaceRegex, '')) + namespaces = namespaces.split('.') + rm(element, type, fn, namespaces) + } else if (typeof typeSpec === 'function') { + // remove(el, fn) + rm(element, null, typeSpec) + } else { + // remove(el, { t1: fn1, t2, fn2 }) + for (k in typeSpec) { + if (typeSpec.hasOwnProperty(k)) + remove(element, k, typeSpec[k]) + } + } + return element + } + + , add = function (element, events, fn, delfn, $) { + var type, types, i, args + , originalFn = fn + , isDel = fn && typeof fn === 'string' + + if (events && !fn && typeof events === 'object') { + for (type in events) { + if (events.hasOwnProperty(type)) + add.apply(this, [ element, type, events[type] ]) + } + } else { + args = arguments.length > 3 ? slice.call(arguments, 3) : [] + types = (isDel ? fn : events).split(' ') + isDel && (fn = del(events, (originalFn = delfn), $)) && (args = slice.call(args, 1)) + // special case for one() + this === ONE && (fn = once(remove, element, events, fn, originalFn)) + for (i = types.length; i--;) addListener(element, types[i], fn, originalFn, args) + } + return element + } + + , one = function () { + return add.apply(ONE, arguments) + } + + , fireListener = W3C_MODEL ? function (isNative, type, element) { + var evt = doc.createEvent(isNative ? 'HTMLEvents' : 'UIEvents') + evt[isNative ? 'initEvent' : 'initUIEvent'](type, true, true, win, 1) + element.dispatchEvent(evt) + } : function (isNative, type, element) { + element = targetElement(element, isNative) + // if not-native then we're using onpropertychange so we just increment a custom property + isNative ? element.fireEvent('on' + type, doc.createEventObject()) : element['_on' + type]++ + } + + , fire = function (element, type, args) { + var i, j, l, names, handlers + , types = type.split(' ') + + for (i = types.length; i--;) { + type = types[i].replace(nameRegex, '') + if (names = types[i].replace(namespaceRegex, '')) + names = names.split('.') + if (!names && !args && element[eventSupport]) { + fireListener(nativeEvents[type], type, element) + } else { + // non-native event, either because of a namespace, arguments or a non DOM element + // iterate over all listeners and manually 'fire' + handlers = registry.get(element, type) + args = [false].concat(args) + for (j = 0, l = handlers.length; j < l; j++) { + if (handlers[j].inNamespaces(names)) + handlers[j].handler.apply(element, args) + } + } + } + return element + } + + , clone = function (element, from, type) { + var i = 0 + , handlers = registry.get(from, type) + , l = handlers.length + + for (;i < l; i++) + handlers[i].original && add(element, handlers[i].type, handlers[i].original) + return element + } + + , bean = { + add: add + , one: one + , remove: remove + , clone: clone + , fire: fire + , noConflict: function () { + context[name] = old + return this + } + } + + if (win[attachEvent]) { + // for IE, clean up on unload to avoid leaks + var cleanup = function () { + var i, entries = registry.entries() + for (i in entries) { + if (entries[i].type && entries[i].type !== 'unload') + remove(entries[i].element, entries[i].type) + } + win[detachEvent]('onunload', cleanup) + win.CollectGarbage && win.CollectGarbage() + } + win[attachEvent]('onunload', cleanup) + } + + return bean +}); + +/** + * Flotr2 (c) 2012 Carl Sutherland + * MIT License + * Special thanks to: + * Flotr: http://code.google.com/p/flotr/ (fork) + * Flot: https://github.com/flot/flot (original fork) + */ +(function () { + +var + global = this, + previousFlotr = this.Flotr, + Flotr; + +Flotr = { + _: _, + bean: bean, + isIphone: /iphone/i.test(navigator.userAgent), + isIE: (navigator.appVersion.indexOf("MSIE") != -1 ? parseFloat(navigator.appVersion.split("MSIE")[1]) : false), + + /** + * An object of the registered graph types. Use Flotr.addType(type, object) + * to add your own type. + */ + graphTypes: {}, + + /** + * The list of the registered plugins + */ + plugins: {}, + + /** + * Can be used to add your own chart type. + * @param {String} name - Type of chart, like 'pies', 'bars' etc. + * @param {String} graphType - The object containing the basic drawing functions (draw, etc) + */ + addType: function(name, graphType){ + Flotr.graphTypes[name] = graphType; + Flotr.defaultOptions[name] = graphType.options || {}; + Flotr.defaultOptions.defaultType = Flotr.defaultOptions.defaultType || name; + }, + + /** + * Can be used to add a plugin + * @param {String} name - The name of the plugin + * @param {String} plugin - The object containing the plugin's data (callbacks, options, function1, function2, ...) + */ + addPlugin: function(name, plugin){ + Flotr.plugins[name] = plugin; + Flotr.defaultOptions[name] = plugin.options || {}; + }, + + /** + * Draws the graph. This function is here for backwards compatibility with Flotr version 0.1.0alpha. + * You could also draw graphs by directly calling Flotr.Graph(element, data, options). + * @param {Element} el - element to insert the graph into + * @param {Object} data - an array or object of dataseries + * @param {Object} options - an object containing options + * @param {Class} _GraphKlass_ - (optional) Class to pass the arguments to, defaults to Flotr.Graph + * @return {Object} returns a new graph object and of course draws the graph. + */ + draw: function(el, data, options, GraphKlass){ + GraphKlass = GraphKlass || Flotr.Graph; + return new GraphKlass(el, data, options); + }, + + /** + * Recursively merges two objects. + * @param {Object} src - source object (likely the object with the least properties) + * @param {Object} dest - destination object (optional, object with the most properties) + * @return {Object} recursively merged Object + * @TODO See if we can't remove this. + */ + merge: function(src, dest){ + var i, v, result = dest || {}; + + for (i in src) { + v = src[i]; + if (v && typeof(v) === 'object') { + if (v.constructor === Array) { + result[i] = this._.clone(v); + } else if (v.constructor !== RegExp && !this._.isElement(v)) { + result[i] = Flotr.merge(v, (dest ? dest[i] : undefined)); + } else { + result[i] = v; + } + } else { + result[i] = v; + } + } + + return result; + }, + + /** + * Recursively clones an object. + * @param {Object} object - The object to clone + * @return {Object} the clone + * @TODO See if we can't remove this. + */ + clone: function(object){ + return Flotr.merge(object, {}); + }, + + /** + * Function calculates the ticksize and returns it. + * @param {Integer} noTicks - number of ticks + * @param {Integer} min - lower bound integer value for the current axis + * @param {Integer} max - upper bound integer value for the current axis + * @param {Integer} decimals - number of decimals for the ticks + * @return {Integer} returns the ticksize in pixels + */ + getTickSize: function(noTicks, min, max, decimals){ + var delta = (max - min) / noTicks, + magn = Flotr.getMagnitude(delta), + tickSize = 10, + norm = delta / magn; // Norm is between 1.0 and 10.0. + + if(norm < 1.5) tickSize = 1; + else if(norm < 2.25) tickSize = 2; + else if(norm < 3) tickSize = ((decimals === 0) ? 2 : 2.5); + else if(norm < 7.5) tickSize = 5; + + return tickSize * magn; + }, + + /** + * Default tick formatter. + * @param {String, Integer} val - tick value integer + * @param {Object} axisOpts - the axis' options + * @return {String} formatted tick string + */ + defaultTickFormatter: function(val, axisOpts){ + return val+''; + }, + + /** + * Formats the mouse tracker values. + * @param {Object} obj - Track value Object {x:..,y:..} + * @return {String} Formatted track string + */ + defaultTrackFormatter: function(obj){ + return '('+obj.x+', '+obj.y+')'; + }, + + /** + * Utility function to convert file size values in bytes to kB, MB, ... + * @param value {Number} - The value to convert + * @param precision {Number} - The number of digits after the comma (default: 2) + * @param base {Number} - The base (default: 1000) + */ + engineeringNotation: function(value, precision, base){ + var sizes = ['Y','Z','E','P','T','G','M','k',''], + fractionSizes = ['y','z','a','f','p','n','µ','m',''], + total = sizes.length; + + base = base || 1000; + precision = Math.pow(10, precision || 2); + + if (value === 0) return 0; + + if (value > 1) { + while (total-- && (value >= base)) value /= base; + } + else { + sizes = fractionSizes; + total = sizes.length; + while (total-- && (value < 1)) value *= base; + } + + return (Math.round(value * precision) / precision) + sizes[total]; + }, + + /** + * Returns the magnitude of the input value. + * @param {Integer, Float} x - integer or float value + * @return {Integer, Float} returns the magnitude of the input value + */ + getMagnitude: function(x){ + return Math.pow(10, Math.floor(Math.log(x) / Math.LN10)); + }, + toPixel: function(val){ + return Math.floor(val)+0.5;//((val-Math.round(val) < 0.4) ? (Math.floor(val)-0.5) : val); + }, + toRad: function(angle){ + return -angle * (Math.PI/180); + }, + floorInBase: function(n, base) { + return base * Math.floor(n / base); + }, + drawText: function(ctx, text, x, y, style) { + if (!ctx.fillText) { + ctx.drawText(text, x, y, style); + return; + } + + style = this._.extend({ + size: Flotr.defaultOptions.fontSize, + color: '#000000', + textAlign: 'left', + textBaseline: 'bottom', + weight: 1, + angle: 0 + }, style); + + ctx.save(); + ctx.translate(x, y); + ctx.rotate(style.angle); + ctx.fillStyle = style.color; + ctx.font = (style.weight > 1 ? "bold " : "") + (style.size*1.3) + "px sans-serif"; + ctx.textAlign = style.textAlign; + ctx.textBaseline = style.textBaseline; + ctx.fillText(text, 0, 0); + ctx.restore(); + }, + getBestTextAlign: function(angle, style) { + style = style || {textAlign: 'center', textBaseline: 'middle'}; + angle += Flotr.getTextAngleFromAlign(style); + + if (Math.abs(Math.cos(angle)) > 10e-3) + style.textAlign = (Math.cos(angle) > 0 ? 'right' : 'left'); + + if (Math.abs(Math.sin(angle)) > 10e-3) + style.textBaseline = (Math.sin(angle) > 0 ? 'top' : 'bottom'); + + return style; + }, + alignTable: { + 'right middle' : 0, + 'right top' : Math.PI/4, + 'center top' : Math.PI/2, + 'left top' : 3*(Math.PI/4), + 'left middle' : Math.PI, + 'left bottom' : -3*(Math.PI/4), + 'center bottom': -Math.PI/2, + 'right bottom' : -Math.PI/4, + 'center middle': 0 + }, + getTextAngleFromAlign: function(style) { + return Flotr.alignTable[style.textAlign+' '+style.textBaseline] || 0; + }, + noConflict : function () { + global.Flotr = previousFlotr; + return this; + } +}; + +global.Flotr = Flotr; + +})(); + +/** + * Flotr Defaults + */ +Flotr.defaultOptions = { + colors: ['#00A8F0', '#C0D800', '#CB4B4B', '#4DA74D', '#9440ED'], //=> The default colorscheme. When there are > 5 series, additional colors are generated. + ieBackgroundColor: '#FFFFFF', // Background color for excanvas clipping + title: null, // => The graph's title + subtitle: null, // => The graph's subtitle + shadowSize: 4, // => size of the 'fake' shadow + defaultType: null, // => default series type + HtmlText: true, // => wether to draw the text using HTML or on the canvas + fontColor: '#545454', // => default font color + fontSize: 7.5, // => canvas' text font size + resolution: 1, // => resolution of the graph, to have printer-friendly graphs ! + parseFloat: true, // => whether to preprocess data for floats (ie. if input is string) + preventDefault: true, // => preventDefault by default for mobile events. Turn off to enable scroll. + xaxis: { + ticks: null, // => format: either [1, 3] or [[1, 'a'], 3] + minorTicks: null, // => format: either [1, 3] or [[1, 'a'], 3] + showLabels: true, // => setting to true will show the axis ticks labels, hide otherwise + showMinorLabels: false,// => true to show the axis minor ticks labels, false to hide + labelsAngle: 0, // => labels' angle, in degrees + title: null, // => axis title + titleAngle: 0, // => axis title's angle, in degrees + noTicks: 5, // => number of ticks for automagically generated ticks + minorTickFreq: null, // => number of minor ticks between major ticks for autogenerated ticks + tickFormatter: Flotr.defaultTickFormatter, // => fn: number, Object -> string + tickDecimals: null, // => no. of decimals, null means auto + min: null, // => min. value to show, null means set automatically + max: null, // => max. value to show, null means set automatically + autoscale: false, // => Turns autoscaling on with true + autoscaleMargin: 0, // => margin in % to add if auto-setting min/max + color: null, // => color of the ticks + mode: 'normal', // => can be 'time' or 'normal' + timeFormat: null, + timeMode:'UTC', // => For UTC time ('local' for local time). + timeUnit:'millisecond',// => Unit for time (millisecond, second, minute, hour, day, month, year) + scaling: 'linear', // => Scaling, can be 'linear' or 'logarithmic' + base: Math.E, + titleAlign: 'center', + margin: true // => Turn off margins with false + }, + x2axis: {}, + yaxis: { + ticks: null, // => format: either [1, 3] or [[1, 'a'], 3] + minorTicks: null, // => format: either [1, 3] or [[1, 'a'], 3] + showLabels: true, // => setting to true will show the axis ticks labels, hide otherwise + showMinorLabels: false,// => true to show the axis minor ticks labels, false to hide + labelsAngle: 0, // => labels' angle, in degrees + title: null, // => axis title + titleAngle: 90, // => axis title's angle, in degrees + noTicks: 5, // => number of ticks for automagically generated ticks + minorTickFreq: null, // => number of minor ticks between major ticks for autogenerated ticks + tickFormatter: Flotr.defaultTickFormatter, // => fn: number, Object -> string + tickDecimals: null, // => no. of decimals, null means auto + min: null, // => min. value to show, null means set automatically + max: null, // => max. value to show, null means set automatically + autoscale: false, // => Turns autoscaling on with true + autoscaleMargin: 0, // => margin in % to add if auto-setting min/max + color: null, // => The color of the ticks + scaling: 'linear', // => Scaling, can be 'linear' or 'logarithmic' + base: Math.E, + titleAlign: 'center', + margin: true // => Turn off margins with false + }, + y2axis: { + titleAngle: 270 + }, + grid: { + color: '#545454', // => primary color used for outline and labels + backgroundColor: null, // => null for transparent, else color + backgroundImage: null, // => background image. String or object with src, left and top + watermarkAlpha: 0.4, // => + tickColor: '#DDDDDD', // => color used for the ticks + labelMargin: 3, // => margin in pixels + verticalLines: true, // => whether to show gridlines in vertical direction + minorVerticalLines: null, // => whether to show gridlines for minor ticks in vertical dir. + horizontalLines: true, // => whether to show gridlines in horizontal direction + minorHorizontalLines: null, // => whether to show gridlines for minor ticks in horizontal dir. + outlineWidth: 1, // => width of the grid outline/border in pixels + outline : 'nsew', // => walls of the outline to display + circular: false // => if set to true, the grid will be circular, must be used when radars are drawn + }, + mouse: { + track: false, // => true to track the mouse, no tracking otherwise + trackAll: false, + position: 'se', // => position of the value box (default south-east) + relative: false, // => next to the mouse cursor + trackFormatter: Flotr.defaultTrackFormatter, // => formats the values in the value box + margin: 5, // => margin in pixels of the valuebox + lineColor: '#FF3F19', // => line color of points that are drawn when mouse comes near a value of a series + trackDecimals: 1, // => decimals for the track values + sensibility: 2, // => the lower this number, the more precise you have to aim to show a value + trackY: true, // => whether or not to track the mouse in the y axis + radius: 3, // => radius of the track point + fillColor: null, // => color to fill our select bar with only applies to bar and similar graphs (only bars for now) + fillOpacity: 0.4 // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill + } +}; + +/** + * Flotr Color + */ + +(function () { + +var + _ = Flotr._; + +// Constructor +function Color (r, g, b, a) { + this.rgba = ['r','g','b','a']; + var x = 4; + while(-1<--x){ + this[this.rgba[x]] = arguments[x] || ((x==3) ? 1.0 : 0); + } + this.normalize(); +} + +// Constants +var COLOR_NAMES = { + aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255], + brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169], + darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47], + darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122], + darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130], + khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144], + lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255], + maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128], + violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0] +}; + +Color.prototype = { + scale: function(rf, gf, bf, af){ + var x = 4; + while (-1 < --x) { + if (!_.isUndefined(arguments[x])) this[this.rgba[x]] *= arguments[x]; + } + return this.normalize(); + }, + alpha: function(alpha) { + if (!_.isUndefined(alpha) && !_.isNull(alpha)) { + this.a = alpha; + } + return this.normalize(); + }, + clone: function(){ + return new Color(this.r, this.b, this.g, this.a); + }, + limit: function(val,minVal,maxVal){ + return Math.max(Math.min(val, maxVal), minVal); + }, + normalize: function(){ + var limit = this.limit; + this.r = limit(parseInt(this.r, 10), 0, 255); + this.g = limit(parseInt(this.g, 10), 0, 255); + this.b = limit(parseInt(this.b, 10), 0, 255); + this.a = limit(this.a, 0, 1); + return this; + }, + distance: function(color){ + if (!color) return; + color = new Color.parse(color); + var dist = 0, x = 3; + while(-1<--x){ + dist += Math.abs(this[this.rgba[x]] - color[this.rgba[x]]); + } + return dist; + }, + toString: function(){ + return (this.a >= 1.0) ? 'rgb('+[this.r,this.g,this.b].join(',')+')' : 'rgba('+[this.r,this.g,this.b,this.a].join(',')+')'; + }, + contrast: function () { + var + test = 1 - ( 0.299 * this.r + 0.587 * this.g + 0.114 * this.b) / 255; + return (test < 0.5 ? '#000000' : '#ffffff'); + } +}; + +_.extend(Color, { + /** + * Parses a color string and returns a corresponding Color. + * The different tests are in order of probability to improve speed. + * @param {String, Color} str - string thats representing a color + * @return {Color} returns a Color object or false + */ + parse: function(color){ + if (color instanceof Color) return color; + + var result; + + // #a0b1c2 + if((result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color))) + return new Color(parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)); + + // rgb(num,num,num) + if((result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color))) + return new Color(parseInt(result[1], 10), parseInt(result[2], 10), parseInt(result[3], 10)); + + // #fff + if((result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color))) + return new Color(parseInt(result[1]+result[1],16), parseInt(result[2]+result[2],16), parseInt(result[3]+result[3],16)); + + // rgba(num,num,num,num) + if((result = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(color))) + return new Color(parseInt(result[1], 10), parseInt(result[2], 10), parseInt(result[3], 10), parseFloat(result[4])); + + // rgb(num%,num%,num%) + if((result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(color))) + return new Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55); + + // rgba(num%,num%,num%,num) + if((result = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(color))) + return new Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55, parseFloat(result[4])); + + // Otherwise, we're most likely dealing with a named color. + var name = (color+'').replace(/^\s*([\S\s]*?)\s*$/, '$1').toLowerCase(); + if(name == 'transparent'){ + return new Color(255, 255, 255, 0); + } + return (result = COLOR_NAMES[name]) ? new Color(result[0], result[1], result[2]) : new Color(0, 0, 0, 0); + }, + + /** + * Process color and options into color style. + */ + processColor: function(color, options) { + + var opacity = options.opacity; + if (!color) return 'rgba(0, 0, 0, 0)'; + if (color instanceof Color) return color.alpha(opacity).toString(); + if (_.isString(color)) return Color.parse(color).alpha(opacity).toString(); + + var grad = color.colors ? color : {colors: color}; + + if (!options.ctx) { + if (!_.isArray(grad.colors)) return 'rgba(0, 0, 0, 0)'; + return Color.parse(_.isArray(grad.colors[0]) ? grad.colors[0][1] : grad.colors[0]).alpha(opacity).toString(); + } + grad = _.extend({start: 'top', end: 'bottom'}, grad); + + if (/top/i.test(grad.start)) options.x1 = 0; + if (/left/i.test(grad.start)) options.y1 = 0; + if (/bottom/i.test(grad.end)) options.x2 = 0; + if (/right/i.test(grad.end)) options.y2 = 0; + + var i, c, stop, gradient = options.ctx.createLinearGradient(options.x1, options.y1, options.x2, options.y2); + for (i = 0; i < grad.colors.length; i++) { + c = grad.colors[i]; + if (_.isArray(c)) { + stop = c[0]; + c = c[1]; + } + else stop = i / (grad.colors.length-1); + gradient.addColorStop(stop, Color.parse(c).alpha(opacity)); + } + return gradient; + } +}); + +Flotr.Color = Color; + +})(); + +/** + * Flotr Date + */ +Flotr.Date = { + + set : function (date, name, mode, value) { + mode = mode || 'UTC'; + name = 'set' + (mode === 'UTC' ? 'UTC' : '') + name; + date[name](value); + }, + + get : function (date, name, mode) { + mode = mode || 'UTC'; + name = 'get' + (mode === 'UTC' ? 'UTC' : '') + name; + return date[name](); + }, + + format: function(d, format, mode) { + if (!d) return; + + // We should maybe use an "official" date format spec, like PHP date() or ColdFusion + // http://fr.php.net/manual/en/function.date.php + // http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=functions_c-d_29.html + var + get = this.get, + tokens = { + h: get(d, 'Hours', mode).toString(), + H: leftPad(get(d, 'Hours', mode)), + M: leftPad(get(d, 'Minutes', mode)), + S: leftPad(get(d, 'Seconds', mode)), + s: get(d, 'Milliseconds', mode), + d: get(d, 'Date', mode).toString(), + m: (get(d, 'Month', mode) + 1).toString(), + y: get(d, 'FullYear', mode).toString(), + b: Flotr.Date.monthNames[get(d, 'Month', mode)] + }; + + function leftPad(n){ + n += ''; + return n.length == 1 ? "0" + n : n; + } + + var r = [], c, + escape = false; + + for (var i = 0; i < format.length; ++i) { + c = format.charAt(i); + + if (escape) { + r.push(tokens[c] || c); + escape = false; + } + else if (c == "%") + escape = true; + else + r.push(c); + } + return r.join(''); + }, + getFormat: function(time, span) { + var tu = Flotr.Date.timeUnits; + if (time < tu.second) return "%h:%M:%S.%s"; + else if (time < tu.minute) return "%h:%M:%S"; + else if (time < tu.day) return (span < 2 * tu.day) ? "%h:%M" : "%b %d %h:%M"; + else if (time < tu.month) return "%b %d"; + else if (time < tu.year) return (span < tu.year) ? "%b" : "%b %y"; + else return "%y"; + }, + formatter: function (v, axis) { + var + options = axis.options, + scale = Flotr.Date.timeUnits[options.timeUnit], + d = new Date(v * scale); + + // first check global format + if (axis.options.timeFormat) + return Flotr.Date.format(d, options.timeFormat, options.timeMode); + + var span = (axis.max - axis.min) * scale, + t = axis.tickSize * Flotr.Date.timeUnits[axis.tickUnit]; + + return Flotr.Date.format(d, Flotr.Date.getFormat(t, span), options.timeMode); + }, + generator: function(axis) { + + var + set = this.set, + get = this.get, + timeUnits = this.timeUnits, + spec = this.spec, + options = axis.options, + mode = options.timeMode, + scale = timeUnits[options.timeUnit], + min = axis.min * scale, + max = axis.max * scale, + delta = (max - min) / options.noTicks, + ticks = [], + tickSize = axis.tickSize, + tickUnit, + formatter, i; + + // Use custom formatter or time tick formatter + formatter = (options.tickFormatter === Flotr.defaultTickFormatter ? + this.formatter : options.tickFormatter + ); + + for (i = 0; i < spec.length - 1; ++i) { + var d = spec[i][0] * timeUnits[spec[i][1]]; + if (delta < (d + spec[i+1][0] * timeUnits[spec[i+1][1]]) / 2 && d >= tickSize) + break; + } + tickSize = spec[i][0]; + tickUnit = spec[i][1]; + + // special-case the possibility of several years + if (tickUnit == "year") { + tickSize = Flotr.getTickSize(options.noTicks*timeUnits.year, min, max, 0); + + // Fix for 0.5 year case + if (tickSize == 0.5) { + tickUnit = "month"; + tickSize = 6; + } + } + + axis.tickUnit = tickUnit; + axis.tickSize = tickSize; + + var step = tickSize * timeUnits[tickUnit]; + d = new Date(min); + + function setTick (name) { + set(d, name, mode, Flotr.floorInBase( + get(d, name, mode), tickSize + )); + } + + switch (tickUnit) { + case "millisecond": setTick('Milliseconds'); break; + case "second": setTick('Seconds'); break; + case "minute": setTick('Minutes'); break; + case "hour": setTick('Hours'); break; + case "month": setTick('Month'); break; + case "year": setTick('FullYear'); break; + } + + // reset smaller components + if (step >= timeUnits.second) set(d, 'Milliseconds', mode, 0); + if (step >= timeUnits.minute) set(d, 'Seconds', mode, 0); + if (step >= timeUnits.hour) set(d, 'Minutes', mode, 0); + if (step >= timeUnits.day) set(d, 'Hours', mode, 0); + if (step >= timeUnits.day * 4) set(d, 'Date', mode, 1); + if (step >= timeUnits.year) set(d, 'Month', mode, 0); + + var carry = 0, v = NaN, prev; + do { + prev = v; + v = d.getTime(); + ticks.push({ v: v / scale, label: formatter(v / scale, axis) }); + if (tickUnit == "month") { + if (tickSize < 1) { + /* a bit complicated - we'll divide the month up but we need to take care of fractions + so we don't end up in the middle of a day */ + set(d, 'Date', mode, 1); + var start = d.getTime(); + set(d, 'Month', mode, get(d, 'Month', mode) + 1); + var end = d.getTime(); + d.setTime(v + carry * timeUnits.hour + (end - start) * tickSize); + carry = get(d, 'Hours', mode); + set(d, 'Hours', mode, 0); + } + else + set(d, 'Month', mode, get(d, 'Month', mode) + tickSize); + } + else if (tickUnit == "year") { + set(d, 'FullYear', mode, get(d, 'FullYear', mode) + tickSize); + } + else + d.setTime(v + step); + + } while (v < max && v != prev); + + return ticks; + }, + timeUnits: { + millisecond: 1, + second: 1000, + minute: 1000 * 60, + hour: 1000 * 60 * 60, + day: 1000 * 60 * 60 * 24, + month: 1000 * 60 * 60 * 24 * 30, + year: 1000 * 60 * 60 * 24 * 365.2425 + }, + // the allowed tick sizes, after 1 year we use an integer algorithm + spec: [ + [1, "millisecond"], [20, "millisecond"], [50, "millisecond"], [100, "millisecond"], [200, "millisecond"], [500, "millisecond"], + [1, "second"], [2, "second"], [5, "second"], [10, "second"], [30, "second"], + [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], [30, "minute"], + [1, "hour"], [2, "hour"], [4, "hour"], [8, "hour"], [12, "hour"], + [1, "day"], [2, "day"], [3, "day"], + [0.25, "month"], [0.5, "month"], [1, "month"], [2, "month"], [3, "month"], [6, "month"], + [1, "year"] + ], + monthNames: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] +}; + +(function () { + +var _ = Flotr._; + +Flotr.DOM = { + addClass: function(element, name){ + var classList = (element.className ? element.className : ''); + if (_.include(classList.split(/\s+/g), name)) return; + element.className = (classList ? classList + ' ' : '') + name; + }, + /** + * Create an element. + */ + create: function(tag){ + return document.createElement(tag); + }, + node: function(html) { + var div = Flotr.DOM.create('div'), n; + div.innerHTML = html; + n = div.children[0]; + div.innerHTML = ''; + return n; + }, + /** + * Remove all children. + */ + empty: function(element){ + element.innerHTML = ''; + /* + if (!element) return; + _.each(element.childNodes, function (e) { + Flotr.DOM.empty(e); + element.removeChild(e); + }); + */ + }, + hide: function(element){ + Flotr.DOM.setStyles(element, {display:'none'}); + }, + /** + * Insert a child. + * @param {Element} element + * @param {Element|String} Element or string to be appended. + */ + insert: function(element, child){ + if(_.isString(child)) + element.innerHTML += child; + else if (_.isElement(child)) + element.appendChild(child); + }, + // @TODO find xbrowser implementation + opacity: function(element, opacity) { + element.style.opacity = opacity; + }, + position: function(element, p){ + if (!element.offsetParent) + return {left: (element.offsetLeft || 0), top: (element.offsetTop || 0)}; + + p = this.position(element.offsetParent); + p.left += element.offsetLeft; + p.top += element.offsetTop; + return p; + }, + removeClass: function(element, name) { + var classList = (element.className ? element.className : ''); + element.className = _.filter(classList.split(/\s+/g), function (c) { + if (c != name) return true; } + ).join(' '); + }, + setStyles: function(element, o) { + _.each(o, function (value, key) { + element.style[key] = value; + }); + }, + show: function(element){ + Flotr.DOM.setStyles(element, {display:''}); + }, + /** + * Return element size. + */ + size: function(element){ + return { + height : element.offsetHeight, + width : element.offsetWidth }; + } +}; + +})(); + +/** + * Flotr Event Adapter + */ +(function () { +var + F = Flotr, + bean = F.bean; +F.EventAdapter = { + observe: function(object, name, callback) { + bean.add(object, name, callback); + return this; + }, + fire: function(object, name, args) { + bean.fire(object, name, args); + if (typeof(Prototype) != 'undefined') + Event.fire(object, name, args); + // @TODO Someone who uses mootools, add mootools adapter for existing applciations. + return this; + }, + stopObserving: function(object, name, callback) { + bean.remove(object, name, callback); + return this; + }, + eventPointer: function(e) { + if (!F._.isUndefined(e.touches) && e.touches.length > 0) { + return { + x : e.touches[0].pageX, + y : e.touches[0].pageY + }; + } else if (!F._.isUndefined(e.changedTouches) && e.changedTouches.length > 0) { + return { + x : e.changedTouches[0].pageX, + y : e.changedTouches[0].pageY + }; + } else if (e.pageX || e.pageY) { + return { + x : e.pageX, + y : e.pageY + }; + } else if (e.clientX || e.clientY) { + var + d = document, + b = d.body, + de = d.documentElement; + return { + x: e.clientX + b.scrollLeft + de.scrollLeft, + y: e.clientY + b.scrollTop + de.scrollTop + }; + } + } +}; +})(); + +/** + * Text Utilities + */ +(function () { + +var + F = Flotr, + D = F.DOM, + _ = F._, + +Text = function (o) { + this.o = o; +}; + +Text.prototype = { + + dimensions : function (text, canvasStyle, htmlStyle, className) { + + if (!text) return { width : 0, height : 0 }; + + return (this.o.html) ? + this.html(text, this.o.element, htmlStyle, className) : + this.canvas(text, canvasStyle); + }, + + canvas : function (text, style) { + + if (!this.o.textEnabled) return; + style = style || {}; + + var + metrics = this.measureText(text, style), + width = metrics.width, + height = style.size || F.defaultOptions.fontSize, + angle = style.angle || 0, + cosAngle = Math.cos(angle), + sinAngle = Math.sin(angle), + widthPadding = 2, + heightPadding = 6, + bounds; + + bounds = { + width: Math.abs(cosAngle * width) + Math.abs(sinAngle * height) + widthPadding, + height: Math.abs(sinAngle * width) + Math.abs(cosAngle * height) + heightPadding + }; + + return bounds; + }, + + html : function (text, element, style, className) { + + var div = D.create('div'); + + D.setStyles(div, { 'position' : 'absolute', 'top' : '-10000px' }); + D.insert(div, '
' + text + '
'); + D.insert(this.o.element, div); + + return D.size(div); + }, + + measureText : function (text, style) { + + var + context = this.o.ctx, + metrics; + + if (!context.fillText || (F.isIphone && context.measure)) { + return { width : context.measure(text, style)}; + } + + style = _.extend({ + size: F.defaultOptions.fontSize, + weight: 1, + angle: 0 + }, style); + + context.save(); + context.font = (style.weight > 1 ? "bold " : "") + (style.size*1.3) + "px sans-serif"; + metrics = context.measureText(text); + context.restore(); + + return metrics; + } +}; + +Flotr.Text = Text; + +})(); + +/** + * Flotr Graph class that plots a graph on creation. + */ +(function () { + +var + D = Flotr.DOM, + E = Flotr.EventAdapter, + _ = Flotr._, + flotr = Flotr; +/** + * Flotr Graph constructor. + * @param {Element} el - element to insert the graph into + * @param {Object} data - an array or object of dataseries + * @param {Object} options - an object containing options + */ +Graph = function(el, data, options){ +// Let's see if we can get away with out this [JS] +// try { + this._setEl(el); + this._initMembers(); + this._initPlugins(); + + E.fire(this.el, 'flotr:beforeinit', [this]); + + this.data = data; + this.series = flotr.Series.getSeries(data); + this._initOptions(options); + this._initGraphTypes(); + this._initCanvas(); + this._text = new flotr.Text({ + element : this.el, + ctx : this.ctx, + html : this.options.HtmlText, + textEnabled : this.textEnabled + }); + E.fire(this.el, 'flotr:afterconstruct', [this]); + this._initEvents(); + + this.findDataRanges(); + this.calculateSpacing(); + + this.draw(_.bind(function() { + E.fire(this.el, 'flotr:afterinit', [this]); + }, this)); +/* + try { + } catch (e) { + try { + console.error(e); + } catch (e2) {} + }*/ +}; + +function observe (object, name, callback) { + E.observe.apply(this, arguments); + this._handles.push(arguments); + return this; +} + +Graph.prototype = { + + destroy: function () { + E.fire(this.el, 'flotr:destroy'); + _.each(this._handles, function (handle) { + E.stopObserving.apply(this, handle); + }); + this._handles = []; + this.el.graph = null; + }, + + observe : observe, + + /** + * @deprecated + */ + _observe : observe, + + processColor: function(color, options){ + var o = { x1: 0, y1: 0, x2: this.plotWidth, y2: this.plotHeight, opacity: 1, ctx: this.ctx }; + _.extend(o, options); + return flotr.Color.processColor(color, o); + }, + /** + * Function determines the min and max values for the xaxis and yaxis. + * + * TODO logarithmic range validation (consideration of 0) + */ + findDataRanges: function(){ + var a = this.axes, + xaxis, yaxis, range; + + _.each(this.series, function (series) { + range = series.getRange(); + if (range) { + xaxis = series.xaxis; + yaxis = series.yaxis; + xaxis.datamin = Math.min(range.xmin, xaxis.datamin); + xaxis.datamax = Math.max(range.xmax, xaxis.datamax); + yaxis.datamin = Math.min(range.ymin, yaxis.datamin); + yaxis.datamax = Math.max(range.ymax, yaxis.datamax); + xaxis.used = (xaxis.used || range.xused); + yaxis.used = (yaxis.used || range.yused); + } + }, this); + + // Check for empty data, no data case (none used) + if (!a.x.used && !a.x2.used) a.x.used = true; + if (!a.y.used && !a.y2.used) a.y.used = true; + + _.each(a, function (axis) { + axis.calculateRange(); + }); + + var + types = _.keys(flotr.graphTypes), + drawn = false; + + _.each(this.series, function (series) { + if (series.hide) return; + _.each(types, function (type) { + if (series[type] && series[type].show) { + this.extendRange(type, series); + drawn = true; + } + }, this); + if (!drawn) { + this.extendRange(this.options.defaultType, series); + } + }, this); + }, + + extendRange : function (type, series) { + if (this[type].extendRange) this[type].extendRange(series, series.data, series[type], this[type]); + if (this[type].extendYRange) this[type].extendYRange(series.yaxis, series.data, series[type], this[type]); + if (this[type].extendXRange) this[type].extendXRange(series.xaxis, series.data, series[type], this[type]); + }, + + /** + * Calculates axis label sizes. + */ + calculateSpacing: function(){ + + var a = this.axes, + options = this.options, + series = this.series, + margin = options.grid.labelMargin, + T = this._text, + x = a.x, + x2 = a.x2, + y = a.y, + y2 = a.y2, + maxOutset = options.grid.outlineWidth, + i, j, l, dim; + + // TODO post refactor, fix this + _.each(a, function (axis) { + axis.calculateTicks(); + axis.calculateTextDimensions(T, options); + }); + + // Title height + dim = T.dimensions( + options.title, + {size: options.fontSize*1.5}, + 'font-size:1em;font-weight:bold;', + 'flotr-title' + ); + this.titleHeight = dim.height; + + // Subtitle height + dim = T.dimensions( + options.subtitle, + {size: options.fontSize}, + 'font-size:smaller;', + 'flotr-subtitle' + ); + this.subtitleHeight = dim.height; + + for(j = 0; j < options.length; ++j){ + if (series[j].points.show){ + maxOutset = Math.max(maxOutset, series[j].points.radius + series[j].points.lineWidth/2); + } + } + + var p = this.plotOffset; + if (x.options.margin === false) { + p.bottom = 0; + p.top = 0; + } else { + p.bottom += (options.grid.circular ? 0 : (x.used && x.options.showLabels ? (x.maxLabel.height + margin) : 0)) + + (x.used && x.options.title ? (x.titleSize.height + margin) : 0) + maxOutset; + + p.top += (options.grid.circular ? 0 : (x2.used && x2.options.showLabels ? (x2.maxLabel.height + margin) : 0)) + + (x2.used && x2.options.title ? (x2.titleSize.height + margin) : 0) + this.subtitleHeight + this.titleHeight + maxOutset; + } + if (y.options.margin === false) { + p.left = 0; + p.right = 0; + } else { + p.left += (options.grid.circular ? 0 : (y.used && y.options.showLabels ? (y.maxLabel.width + margin) : 0)) + + (y.used && y.options.title ? (y.titleSize.width + margin) : 0) + maxOutset; + + p.right += (options.grid.circular ? 0 : (y2.used && y2.options.showLabels ? (y2.maxLabel.width + margin) : 0)) + + (y2.used && y2.options.title ? (y2.titleSize.width + margin) : 0) + maxOutset; + } + + p.top = Math.floor(p.top); // In order the outline not to be blured + + this.plotWidth = this.canvasWidth - p.left - p.right; + this.plotHeight = this.canvasHeight - p.bottom - p.top; + + // TODO post refactor, fix this + x.length = x2.length = this.plotWidth; + y.length = y2.length = this.plotHeight; + y.offset = y2.offset = this.plotHeight; + x.setScale(); + x2.setScale(); + y.setScale(); + y2.setScale(); + }, + /** + * Draws grid, labels, series and outline. + */ + draw: function(after) { + + var + context = this.ctx, + i; + + E.fire(this.el, 'flotr:beforedraw', [this.series, this]); + + if (this.series.length) { + + context.save(); + context.translate(this.plotOffset.left, this.plotOffset.top); + + for (i = 0; i < this.series.length; i++) { + if (!this.series[i].hide) this.drawSeries(this.series[i]); + } + + context.restore(); + this.clip(); + } + + E.fire(this.el, 'flotr:afterdraw', [this.series, this]); + if (after) after(); + }, + /** + * Actually draws the graph. + * @param {Object} series - series to draw + */ + drawSeries: function(series){ + + function drawChart (series, typeKey) { + var options = this.getOptions(series, typeKey); + this[typeKey].draw(options); + } + + var drawn = false; + series = series || this.series; + + _.each(flotr.graphTypes, function (type, typeKey) { + if (series[typeKey] && series[typeKey].show && this[typeKey]) { + drawn = true; + drawChart.call(this, series, typeKey); + } + }, this); + + if (!drawn) drawChart.call(this, series, this.options.defaultType); + }, + + getOptions : function (series, typeKey) { + var + type = series[typeKey], + graphType = this[typeKey], + xaxis = series.xaxis, + yaxis = series.yaxis, + options = { + context : this.ctx, + width : this.plotWidth, + height : this.plotHeight, + fontSize : this.options.fontSize, + fontColor : this.options.fontColor, + textEnabled : this.textEnabled, + htmlText : this.options.HtmlText, + text : this._text, // TODO Is this necessary? + element : this.el, + data : series.data, + color : series.color, + shadowSize : series.shadowSize, + xScale : xaxis.d2p, + yScale : yaxis.d2p, + xInverse : xaxis.p2d, + yInverse : yaxis.p2d + }; + + options = flotr.merge(type, options); + + // Fill + options.fillStyle = this.processColor( + type.fillColor || series.color, + {opacity: type.fillOpacity} + ); + + return options; + }, + /** + * Calculates the coordinates from a mouse event object. + * @param {Event} event - Mouse Event object. + * @return {Object} Object with coordinates of the mouse. + */ + getEventPosition: function (e){ + + var + d = document, + b = d.body, + de = d.documentElement, + axes = this.axes, + plotOffset = this.plotOffset, + lastMousePos = this.lastMousePos, + pointer = E.eventPointer(e), + dx = pointer.x - lastMousePos.pageX, + dy = pointer.y - lastMousePos.pageY, + r, rx, ry; + + if ('ontouchstart' in this.el) { + r = D.position(this.overlay); + rx = pointer.x - r.left - plotOffset.left; + ry = pointer.y - r.top - plotOffset.top; + } else { + r = this.overlay.getBoundingClientRect(); + rx = e.clientX - r.left - plotOffset.left - b.scrollLeft - de.scrollLeft; + ry = e.clientY - r.top - plotOffset.top - b.scrollTop - de.scrollTop; + } + + return { + x: axes.x.p2d(rx), + x2: axes.x2.p2d(rx), + y: axes.y.p2d(ry), + y2: axes.y2.p2d(ry), + relX: rx, + relY: ry, + dX: dx, + dY: dy, + absX: pointer.x, + absY: pointer.y, + pageX: pointer.x, + pageY: pointer.y + }; + }, + /** + * Observes the 'click' event and fires the 'flotr:click' event. + * @param {Event} event - 'click' Event object. + */ + clickHandler: function(event){ + if(this.ignoreClick){ + this.ignoreClick = false; + return this.ignoreClick; + } + E.fire(this.el, 'flotr:click', [this.getEventPosition(event), this]); + }, + /** + * Observes mouse movement over the graph area. Fires the 'flotr:mousemove' event. + * @param {Event} event - 'mousemove' Event object. + */ + mouseMoveHandler: function(event){ + if (this.mouseDownMoveHandler) return; + var pos = this.getEventPosition(event); + E.fire(this.el, 'flotr:mousemove', [event, pos, this]); + this.lastMousePos = pos; + }, + /** + * Observes the 'mousedown' event. + * @param {Event} event - 'mousedown' Event object. + */ + mouseDownHandler: function (event){ + + /* + // @TODO Context menu? + if(event.isRightClick()) { + event.stop(); + + var overlay = this.overlay; + overlay.hide(); + + function cancelContextMenu () { + overlay.show(); + E.stopObserving(document, 'mousemove', cancelContextMenu); + } + E.observe(document, 'mousemove', cancelContextMenu); + return; + } + */ + + if (this.mouseUpHandler) return; + this.mouseUpHandler = _.bind(function (e) { + E.stopObserving(document, 'mouseup', this.mouseUpHandler); + E.stopObserving(document, 'mousemove', this.mouseDownMoveHandler); + this.mouseDownMoveHandler = null; + this.mouseUpHandler = null; + // @TODO why? + //e.stop(); + E.fire(this.el, 'flotr:mouseup', [e, this]); + }, this); + this.mouseDownMoveHandler = _.bind(function (e) { + var pos = this.getEventPosition(e); + E.fire(this.el, 'flotr:mousemove', [event, pos, this]); + this.lastMousePos = pos; + }, this); + E.observe(document, 'mouseup', this.mouseUpHandler); + E.observe(document, 'mousemove', this.mouseDownMoveHandler); + E.fire(this.el, 'flotr:mousedown', [event, this]); + this.ignoreClick = false; + }, + drawTooltip: function(content, x, y, options) { + var mt = this.getMouseTrack(), + style = 'opacity:0.7;background-color:#000;color:#fff;display:none;position:absolute;padding:2px 8px;-moz-border-radius:4px;border-radius:4px;white-space:nowrap;', + p = options.position, + m = options.margin, + plotOffset = this.plotOffset; + + if(x !== null && y !== null){ + if (!options.relative) { // absolute to the canvas + if(p.charAt(0) == 'n') style += 'top:' + (m + plotOffset.top) + 'px;bottom:auto;'; + else if(p.charAt(0) == 's') style += 'bottom:' + (m + plotOffset.bottom) + 'px;top:auto;'; + if(p.charAt(1) == 'e') style += 'right:' + (m + plotOffset.right) + 'px;left:auto;'; + else if(p.charAt(1) == 'w') style += 'left:' + (m + plotOffset.left) + 'px;right:auto;'; + } + else { // relative to the mouse + if(p.charAt(0) == 'n') style += 'bottom:' + (m - plotOffset.top - y + this.canvasHeight) + 'px;top:auto;'; + else if(p.charAt(0) == 's') style += 'top:' + (m + plotOffset.top + y) + 'px;bottom:auto;'; + if(p.charAt(1) == 'e') style += 'left:' + (m + plotOffset.left + x) + 'px;right:auto;'; + else if(p.charAt(1) == 'w') style += 'right:' + (m - plotOffset.left - x + this.canvasWidth) + 'px;left:auto;'; + } + + mt.style.cssText = style; + D.empty(mt); + D.insert(mt, content); + D.show(mt); + } + else { + D.hide(mt); + } + }, + + clip: function (ctx) { + + var + o = this.plotOffset, + w = this.canvasWidth, + h = this.canvasHeight; + + ctx = ctx || this.ctx; + + if (flotr.isIE && flotr.isIE < 9) { + // Clipping for excanvas :-( + ctx.save(); + ctx.fillStyle = this.processColor(this.options.ieBackgroundColor); + ctx.fillRect(0, 0, w, o.top); + ctx.fillRect(0, 0, o.left, h); + ctx.fillRect(0, h - o.bottom, w, o.bottom); + ctx.fillRect(w - o.right, 0, o.right,h); + ctx.restore(); + } else { + ctx.clearRect(0, 0, w, o.top); + ctx.clearRect(0, 0, o.left, h); + ctx.clearRect(0, h - o.bottom, w, o.bottom); + ctx.clearRect(w - o.right, 0, o.right,h); + } + }, + + _initMembers: function() { + this._handles = []; + this.lastMousePos = {pageX: null, pageY: null }; + this.plotOffset = {left: 0, right: 0, top: 0, bottom: 0}; + this.ignoreClick = true; + this.prevHit = null; + }, + + _initGraphTypes: function() { + _.each(flotr.graphTypes, function(handler, graphType){ + this[graphType] = flotr.clone(handler); + }, this); + }, + + _initEvents: function () { + + var + el = this.el, + touchendHandler, movement, touchend; + + if ('ontouchstart' in el) { + + touchendHandler = _.bind(function (e) { + touchend = true; + E.stopObserving(document, 'touchend', touchendHandler); + E.fire(el, 'flotr:mouseup', [event, this]); + this.multitouches = null; + + if (!movement) { + this.clickHandler(e); + } + }, this); + + this.observe(this.overlay, 'touchstart', _.bind(function (e) { + movement = false; + touchend = false; + this.ignoreClick = false; + + if (e.touches && e.touches.length > 1) { + this.multitouches = e.touches; + } + + E.fire(el, 'flotr:mousedown', [event, this]); + this.observe(document, 'touchend', touchendHandler); + }, this)); + + this.observe(this.overlay, 'touchmove', _.bind(function (e) { + + var pos = this.getEventPosition(e); + + if (this.options.preventDefault) { + e.preventDefault(); + } + + movement = true; + + if (this.multitouches || (e.touches && e.touches.length > 1)) { + this.multitouches = e.touches; + } else { + if (!touchend) { + E.fire(el, 'flotr:mousemove', [event, pos, this]); + } + } + this.lastMousePos = pos; + }, this)); + + } else { + this. + observe(this.overlay, 'mousedown', _.bind(this.mouseDownHandler, this)). + observe(el, 'mousemove', _.bind(this.mouseMoveHandler, this)). + observe(this.overlay, 'click', _.bind(this.clickHandler, this)). + observe(el, 'mouseout', function () { + E.fire(el, 'flotr:mouseout'); + }); + } + }, + + /** + * Initializes the canvas and it's overlay canvas element. When the browser is IE, this makes use + * of excanvas. The overlay canvas is inserted for displaying interactions. After the canvas elements + * are created, the elements are inserted into the container element. + */ + _initCanvas: function(){ + var el = this.el, + o = this.options, + children = el.children, + removedChildren = [], + child, i, + size, style; + + // Empty the el + for (i = children.length; i--;) { + child = children[i]; + if (!this.canvas && child.className === 'flotr-canvas') { + this.canvas = child; + } else if (!this.overlay && child.className === 'flotr-overlay') { + this.overlay = child; + } else { + removedChildren.push(child); + } + } + for (i = removedChildren.length; i--;) { + el.removeChild(removedChildren[i]); + } + + D.setStyles(el, {position: 'relative'}); // For positioning labels and overlay. + size = {}; + size.width = el.clientWidth; + size.height = el.clientHeight; + + if(size.width <= 0 || size.height <= 0 || o.resolution <= 0){ + throw 'Invalid dimensions for plot, width = ' + size.width + ', height = ' + size.height + ', resolution = ' + o.resolution; + } + + // Main canvas for drawing graph types + this.canvas = getCanvas(this.canvas, 'canvas'); + // Overlay canvas for interactive features + this.overlay = getCanvas(this.overlay, 'overlay'); + this.ctx = getContext(this.canvas); + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + this.octx = getContext(this.overlay); + this.octx.clearRect(0, 0, this.overlay.width, this.overlay.height); + this.canvasHeight = size.height; + this.canvasWidth = size.width; + this.textEnabled = !!this.ctx.drawText || !!this.ctx.fillText; // Enable text functions + + function getCanvas(canvas, name){ + if(!canvas){ + canvas = D.create('canvas'); + if (typeof FlashCanvas != "undefined" && typeof canvas.getContext === 'function') { + FlashCanvas.initElement(canvas); + } + canvas.className = 'flotr-'+name; + canvas.style.cssText = 'position:absolute;left:0px;top:0px;'; + D.insert(el, canvas); + } + _.each(size, function(size, attribute){ + D.show(canvas); + if (name == 'canvas' && canvas.getAttribute(attribute) === size) { + return; + } + canvas.setAttribute(attribute, size * o.resolution); + canvas.style[attribute] = size + 'px'; + }); + canvas.context_ = null; // Reset the ExCanvas context + return canvas; + } + + function getContext(canvas){ + if(window.G_vmlCanvasManager) window.G_vmlCanvasManager.initElement(canvas); // For ExCanvas + var context = canvas.getContext('2d'); + if(!window.G_vmlCanvasManager) context.scale(o.resolution, o.resolution); + return context; + } + }, + + _initPlugins: function(){ + // TODO Should be moved to flotr and mixed in. + _.each(flotr.plugins, function(plugin, name){ + _.each(plugin.callbacks, function(fn, c){ + this.observe(this.el, c, _.bind(fn, this)); + }, this); + this[name] = flotr.clone(plugin); + _.each(this[name], function(fn, p){ + if (_.isFunction(fn)) + this[name][p] = _.bind(fn, this); + }, this); + }, this); + }, + + /** + * Sets options and initializes some variables and color specific values, used by the constructor. + * @param {Object} opts - options object + */ + _initOptions: function(opts){ + var options = flotr.clone(flotr.defaultOptions); + options.x2axis = _.extend(_.clone(options.xaxis), options.x2axis); + options.y2axis = _.extend(_.clone(options.yaxis), options.y2axis); + this.options = flotr.merge(opts || {}, options); + + if (this.options.grid.minorVerticalLines === null && + this.options.xaxis.scaling === 'logarithmic') { + this.options.grid.minorVerticalLines = true; + } + if (this.options.grid.minorHorizontalLines === null && + this.options.yaxis.scaling === 'logarithmic') { + this.options.grid.minorHorizontalLines = true; + } + + E.fire(this.el, 'flotr:afterinitoptions', [this]); + + this.axes = flotr.Axis.getAxes(this.options); + + // Initialize some variables used throughout this function. + var assignedColors = [], + colors = [], + ln = this.series.length, + neededColors = this.series.length, + oc = this.options.colors, + usedColors = [], + variation = 0, + c, i, j, s; + + // Collect user-defined colors from series. + for(i = neededColors - 1; i > -1; --i){ + c = this.series[i].color; + if(c){ + --neededColors; + if(_.isNumber(c)) assignedColors.push(c); + else usedColors.push(flotr.Color.parse(c)); + } + } + + // Calculate the number of colors that need to be generated. + for(i = assignedColors.length - 1; i > -1; --i) + neededColors = Math.max(neededColors, assignedColors[i] + 1); + + // Generate needed number of colors. + for(i = 0; colors.length < neededColors;){ + c = (oc.length == i) ? new flotr.Color(100, 100, 100) : flotr.Color.parse(oc[i]); + + // Make sure each serie gets a different color. + var sign = variation % 2 == 1 ? -1 : 1, + factor = 1 + sign * Math.ceil(variation / 2) * 0.2; + c.scale(factor, factor, factor); + + /** + * @todo if we're getting too close to something else, we should probably skip this one + */ + colors.push(c); + + if(++i >= oc.length){ + i = 0; + ++variation; + } + } + + // Fill the options with the generated colors. + for(i = 0, j = 0; i < ln; ++i){ + s = this.series[i]; + + // Assign the color. + if (!s.color){ + s.color = colors[j++].toString(); + }else if(_.isNumber(s.color)){ + s.color = colors[s.color].toString(); + } + + // Every series needs an axis + if (!s.xaxis) s.xaxis = this.axes.x; + if (s.xaxis == 1) s.xaxis = this.axes.x; + else if (s.xaxis == 2) s.xaxis = this.axes.x2; + + if (!s.yaxis) s.yaxis = this.axes.y; + if (s.yaxis == 1) s.yaxis = this.axes.y; + else if (s.yaxis == 2) s.yaxis = this.axes.y2; + + // Apply missing options to the series. + for (var t in flotr.graphTypes){ + s[t] = _.extend(_.clone(this.options[t]), s[t]); + } + s.mouse = _.extend(_.clone(this.options.mouse), s.mouse); + + if (_.isUndefined(s.shadowSize)) s.shadowSize = this.options.shadowSize; + } + }, + + _setEl: function(el) { + if (!el) throw 'The target container doesn\'t exist'; + else if (el.graph instanceof Graph) el.graph.destroy(); + else if (!el.clientWidth) throw 'The target container must be visible'; + + el.graph = this; + this.el = el; + } +}; + +Flotr.Graph = Graph; + +})(); + +/** + * Flotr Axis Library + */ + +(function () { + +var + _ = Flotr._, + LOGARITHMIC = 'logarithmic'; + +function Axis (o) { + + this.orientation = 1; + this.offset = 0; + this.datamin = Number.MAX_VALUE; + this.datamax = -Number.MAX_VALUE; + + _.extend(this, o); +} + + +// Prototype +Axis.prototype = { + + setScale : function () { + var + length = this.length, + max = this.max, + min = this.min, + offset = this.offset, + orientation = this.orientation, + options = this.options, + logarithmic = options.scaling === LOGARITHMIC, + scale; + + if (logarithmic) { + scale = length / (log(max, options.base) - log(min, options.base)); + } else { + scale = length / (max - min); + } + this.scale = scale; + + // Logarithmic? + if (logarithmic) { + this.d2p = function (dataValue) { + return offset + orientation * (log(dataValue, options.base) - log(min, options.base)) * scale; + } + this.p2d = function (pointValue) { + return exp((offset + orientation * pointValue) / scale + log(min, options.base), options.base); + } + } else { + this.d2p = function (dataValue) { + return offset + orientation * (dataValue - min) * scale; + } + this.p2d = function (pointValue) { + return (offset + orientation * pointValue) / scale + min; + } + } + }, + + calculateTicks : function () { + var options = this.options; + + this.ticks = []; + this.minorTicks = []; + + // User Ticks + if(options.ticks){ + this._cleanUserTicks(options.ticks, this.ticks); + this._cleanUserTicks(options.minorTicks || [], this.minorTicks); + } + else { + if (options.mode == 'time') { + this._calculateTimeTicks(); + } else if (options.scaling === 'logarithmic') { + this._calculateLogTicks(); + } else { + this._calculateTicks(); + } + } + + // Ticks to strings + _.each(this.ticks, function (tick) { tick.label += ''; }); + _.each(this.minorTicks, function (tick) { tick.label += ''; }); + }, + + /** + * Calculates the range of an axis to apply autoscaling. + */ + calculateRange: function () { + + if (!this.used) return; + + var axis = this, + o = axis.options, + min = o.min !== null ? o.min : axis.datamin, + max = o.max !== null ? o.max : axis.datamax, + margin = o.autoscaleMargin; + + if (o.scaling == 'logarithmic') { + if (min <= 0) min = axis.datamin; + + // Let it widen later on + if (max <= 0) max = min; + } + + if (max == min) { + var widen = max ? 0.01 : 1.00; + if (o.min === null) min -= widen; + if (o.max === null) max += widen; + } + + if (o.scaling === 'logarithmic') { + if (min < 0) min = max / o.base; // Could be the result of widening + + var maxexp = Math.log(max); + if (o.base != Math.E) maxexp /= Math.log(o.base); + maxexp = Math.ceil(maxexp); + + var minexp = Math.log(min); + if (o.base != Math.E) minexp /= Math.log(o.base); + minexp = Math.ceil(minexp); + + axis.tickSize = Flotr.getTickSize(o.noTicks, minexp, maxexp, o.tickDecimals === null ? 0 : o.tickDecimals); + + // Try to determine a suitable amount of miniticks based on the length of a decade + if (o.minorTickFreq === null) { + if (maxexp - minexp > 10) + o.minorTickFreq = 0; + else if (maxexp - minexp > 5) + o.minorTickFreq = 2; + else + o.minorTickFreq = 5; + } + } else { + axis.tickSize = Flotr.getTickSize(o.noTicks, min, max, o.tickDecimals); + } + + axis.min = min; + axis.max = max; //extendRange may use axis.min or axis.max, so it should be set before it is caled + + // Autoscaling. @todo This probably fails with log scale. Find a testcase and fix it + if(o.min === null && o.autoscale){ + axis.min -= axis.tickSize * margin; + // Make sure we don't go below zero if all values are positive. + if(axis.min < 0 && axis.datamin >= 0) axis.min = 0; + axis.min = axis.tickSize * Math.floor(axis.min / axis.tickSize); + } + + if(o.max === null && o.autoscale){ + axis.max += axis.tickSize * margin; + if(axis.max > 0 && axis.datamax <= 0 && axis.datamax != axis.datamin) axis.max = 0; + axis.max = axis.tickSize * Math.ceil(axis.max / axis.tickSize); + } + + if (axis.min == axis.max) axis.max = axis.min + 1; + }, + + calculateTextDimensions : function (T, options) { + + var maxLabel = '', + length, + i; + + if (this.options.showLabels) { + for (i = 0; i < this.ticks.length; ++i) { + length = this.ticks[i].label.length; + if (length > maxLabel.length){ + maxLabel = this.ticks[i].label; + } + } + } + + this.maxLabel = T.dimensions( + maxLabel, + {size:options.fontSize, angle: Flotr.toRad(this.options.labelsAngle)}, + 'font-size:smaller;', + 'flotr-grid-label' + ); + + this.titleSize = T.dimensions( + this.options.title, + {size:options.fontSize*1.2, angle: Flotr.toRad(this.options.titleAngle)}, + 'font-weight:bold;', + 'flotr-axis-title' + ); + }, + + _cleanUserTicks : function (ticks, axisTicks) { + + var axis = this, options = this.options, + v, i, label, tick; + + if(_.isFunction(ticks)) ticks = ticks({min : axis.min, max : axis.max}); + + for(i = 0; i < ticks.length; ++i){ + tick = ticks[i]; + if(typeof(tick) === 'object'){ + v = tick[0]; + label = (tick.length > 1) ? tick[1] : options.tickFormatter(v, {min : axis.min, max : axis.max}); + } else { + v = tick; + label = options.tickFormatter(v, {min : this.min, max : this.max}); + } + axisTicks[i] = { v: v, label: label }; + } + }, + + _calculateTimeTicks : function () { + this.ticks = Flotr.Date.generator(this); + }, + + _calculateLogTicks : function () { + + var axis = this, + o = axis.options, + v, + decadeStart; + + var max = Math.log(axis.max); + if (o.base != Math.E) max /= Math.log(o.base); + max = Math.ceil(max); + + var min = Math.log(axis.min); + if (o.base != Math.E) min /= Math.log(o.base); + min = Math.ceil(min); + + for (i = min; i < max; i += axis.tickSize) { + decadeStart = (o.base == Math.E) ? Math.exp(i) : Math.pow(o.base, i); + // Next decade begins here: + var decadeEnd = decadeStart * ((o.base == Math.E) ? Math.exp(axis.tickSize) : Math.pow(o.base, axis.tickSize)); + var stepSize = (decadeEnd - decadeStart) / o.minorTickFreq; + + axis.ticks.push({v: decadeStart, label: o.tickFormatter(decadeStart, {min : axis.min, max : axis.max})}); + for (v = decadeStart + stepSize; v < decadeEnd; v += stepSize) + axis.minorTicks.push({v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max})}); + } + + // Always show the value at the would-be start of next decade (end of this decade) + decadeStart = (o.base == Math.E) ? Math.exp(i) : Math.pow(o.base, i); + axis.ticks.push({v: decadeStart, label: o.tickFormatter(decadeStart, {min : axis.min, max : axis.max})}); + }, + + _calculateTicks : function () { + + var axis = this, + o = axis.options, + tickSize = axis.tickSize, + min = axis.min, + max = axis.max, + start = tickSize * Math.ceil(min / tickSize), // Round to nearest multiple of tick size. + decimals, + minorTickSize, + v, v2, + i, j; + + if (o.minorTickFreq) + minorTickSize = tickSize / o.minorTickFreq; + + // Then store all possible ticks. + for (i = 0; (v = v2 = start + i * tickSize) <= max; ++i){ + + // Round (this is always needed to fix numerical instability). + decimals = o.tickDecimals; + if (decimals === null) decimals = 1 - Math.floor(Math.log(tickSize) / Math.LN10); + if (decimals < 0) decimals = 0; + + v = v.toFixed(decimals); + axis.ticks.push({ v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max}) }); + + if (o.minorTickFreq) { + for (j = 0; j < o.minorTickFreq && (i * tickSize + j * minorTickSize) < max; ++j) { + v = v2 + j * minorTickSize; + axis.minorTicks.push({ v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max}) }); + } + } + } + + } +}; + + +// Static Methods +_.extend(Axis, { + getAxes : function (options) { + return { + x: new Axis({options: options.xaxis, n: 1, length: this.plotWidth}), + x2: new Axis({options: options.x2axis, n: 2, length: this.plotWidth}), + y: new Axis({options: options.yaxis, n: 1, length: this.plotHeight, offset: this.plotHeight, orientation: -1}), + y2: new Axis({options: options.y2axis, n: 2, length: this.plotHeight, offset: this.plotHeight, orientation: -1}) + }; + } +}); + + +// Helper Methods + + +function log (value, base) { + value = Math.log(Math.max(value, Number.MIN_VALUE)); + if (base !== Math.E) + value /= Math.log(base); + return value; +} + +function exp (value, base) { + return (base === Math.E) ? Math.exp(value) : Math.pow(base, value); +} + +Flotr.Axis = Axis; + +})(); + +/** + * Flotr Series Library + */ + +(function () { + +var + _ = Flotr._; + +function Series (o) { + _.extend(this, o); +} + +Series.prototype = { + + getRange: function () { + + var + data = this.data, + length = data.length, + xmin = Number.MAX_VALUE, + ymin = Number.MAX_VALUE, + xmax = -Number.MAX_VALUE, + ymax = -Number.MAX_VALUE, + xused = false, + yused = false, + x, y, i; + + if (length < 0 || this.hide) return false; + + for (i = 0; i < length; i++) { + x = data[i][0]; + y = data[i][1]; + if (x !== null) { + if (x < xmin) { xmin = x; xused = true; } + if (x > xmax) { xmax = x; xused = true; } + } + if (y !== null) { + if (y < ymin) { ymin = y; yused = true; } + if (y > ymax) { ymax = y; yused = true; } + } + } + + return { + xmin : xmin, + xmax : xmax, + ymin : ymin, + ymax : ymax, + xused : xused, + yused : yused + }; + } +}; + +_.extend(Series, { + /** + * Collects dataseries from input and parses the series into the right format. It returns an Array + * of Objects each having at least the 'data' key set. + * @param {Array, Object} data - Object or array of dataseries + * @return {Array} Array of Objects parsed into the right format ({(...,) data: [[x1,y1], [x2,y2], ...] (, ...)}) + */ + getSeries: function(data){ + return _.map(data, function(s){ + var series; + if (s.data) { + series = new Series(); + _.extend(series, s); + } else { + series = new Series({data:s}); + } + return series; + }); + } +}); + +Flotr.Series = Series; + +})(); + +/** Lines **/ +Flotr.addType('lines', { + options: { + show: false, // => setting to true will show lines, false will hide + lineWidth: 2, // => line width in pixels + fill: false, // => true to fill the area from the line to the x axis, false for (transparent) no fill + fillBorder: false, // => draw a border around the fill + fillColor: null, // => fill color + fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill + steps: false, // => draw steps + stacked: false // => setting to true will show stacked lines, false will show normal lines + }, + + stack : { + values : [] + }, + + /** + * Draws lines series in the canvas element. + * @param {Object} options + */ + draw : function (options) { + + var + context = options.context, + lineWidth = options.lineWidth, + shadowSize = options.shadowSize, + offset; + + context.save(); + context.lineJoin = 'round'; + + if (shadowSize) { + + context.lineWidth = shadowSize / 2; + offset = lineWidth / 2 + context.lineWidth / 2; + + // @TODO do this instead with a linear gradient + context.strokeStyle = "rgba(0,0,0,0.1)"; + this.plot(options, offset + shadowSize / 2, false); + + context.strokeStyle = "rgba(0,0,0,0.2)"; + this.plot(options, offset, false); + } + + context.lineWidth = lineWidth; + context.strokeStyle = options.color; + + this.plot(options, 0, true); + + context.restore(); + }, + + plot : function (options, shadowOffset, incStack) { + + var + context = options.context, + width = options.width, + height = options.height, + xScale = options.xScale, + yScale = options.yScale, + data = options.data, + stack = options.stacked ? this.stack : false, + length = data.length - 1, + prevx = null, + prevy = null, + zero = yScale(0), + start = null, + x1, x2, y1, y2, stack1, stack2, i; + + if (length < 1) return; + + context.beginPath(); + + for (i = 0; i < length; ++i) { + + // To allow empty values + if (data[i][1] === null || data[i+1][1] === null) { + if (options.fill) { + if (i > 0 && data[i][1]) { + context.stroke(); + fill(); + start = null; + context.closePath(); + context.beginPath(); + } + } + continue; + } + + // Zero is infinity for log scales + // TODO handle zero for logarithmic + // if (xa.options.scaling === 'logarithmic' && (data[i][0] <= 0 || data[i+1][0] <= 0)) continue; + // if (ya.options.scaling === 'logarithmic' && (data[i][1] <= 0 || data[i+1][1] <= 0)) continue; + + x1 = xScale(data[i][0]); + x2 = xScale(data[i+1][0]); + + if (start === null) start = data[i]; + + if (stack) { + + stack1 = stack.values[data[i][0]] || 0; + stack2 = stack.values[data[i+1][0]] || stack.values[data[i][0]] || 0; + + y1 = yScale(data[i][1] + stack1); + y2 = yScale(data[i+1][1] + stack2); + + if(incStack){ + stack.values[data[i][0]] = data[i][1]+stack1; + + if(i == length-1) + stack.values[data[i+1][0]] = data[i+1][1]+stack2; + } + } + else{ + y1 = yScale(data[i][1]); + y2 = yScale(data[i+1][1]); + } + + if ( + (y1 > height && y2 > height) || + (y1 < 0 && y2 < 0) || + (x1 < 0 && x2 < 0) || + (x1 > width && x2 > width) + ) continue; + + if((prevx != x1) || (prevy != y1 + shadowOffset)) + context.moveTo(x1, y1 + shadowOffset); + + prevx = x2; + prevy = y2 + shadowOffset; + if (options.steps) { + context.lineTo(prevx + shadowOffset / 2, y1 + shadowOffset); + context.lineTo(prevx + shadowOffset / 2, prevy); + } else { + context.lineTo(prevx, prevy); + } + } + + if (!options.fill || options.fill && !options.fillBorder) context.stroke(); + + fill(); + + function fill () { + // TODO stacked lines + if(!shadowOffset && options.fill && start){ + x1 = xScale(start[0]); + context.fillStyle = options.fillStyle; + context.lineTo(x2, zero); + context.lineTo(x1, zero); + context.lineTo(x1, yScale(start[1])); + context.fill(); + if (options.fillBorder) { + context.stroke(); + } + } + } + + context.closePath(); + }, + + // Perform any pre-render precalculations (this should be run on data first) + // - Pie chart total for calculating measures + // - Stacks for lines and bars + // precalculate : function () { + // } + // + // + // Get any bounds after pre calculation (axis can fetch this if does not have explicit min/max) + // getBounds : function () { + // } + // getMin : function () { + // } + // getMax : function () { + // } + // + // + // Padding around rendered elements + // getPadding : function () { + // } + + extendYRange : function (axis, data, options, lines) { + + var o = axis.options; + + // If stacked and auto-min + if (options.stacked && ((!o.max && o.max !== 0) || (!o.min && o.min !== 0))) { + + var + newmax = axis.max, + newmin = axis.min, + positiveSums = lines.positiveSums || {}, + negativeSums = lines.negativeSums || {}, + x, j; + + for (j = 0; j < data.length; j++) { + + x = data[j][0] + ''; + + // Positive + if (data[j][1] > 0) { + positiveSums[x] = (positiveSums[x] || 0) + data[j][1]; + newmax = Math.max(newmax, positiveSums[x]); + } + + // Negative + else { + negativeSums[x] = (negativeSums[x] || 0) + data[j][1]; + newmin = Math.min(newmin, negativeSums[x]); + } + } + + lines.negativeSums = negativeSums; + lines.positiveSums = positiveSums; + + axis.max = newmax; + axis.min = newmin; + } + + if (options.steps) { + + this.hit = function (options) { + var + data = options.data, + args = options.args, + yScale = options.yScale, + mouse = args[0], + length = data.length, + n = args[1], + x = options.xInverse(mouse.relX), + relY = mouse.relY, + i; + + for (i = 0; i < length - 1; i++) { + if (x >= data[i][0] && x <= data[i+1][0]) { + if (Math.abs(yScale(data[i][1]) - relY) < 8) { + n.x = data[i][0]; + n.y = data[i][1]; + n.index = i; + n.seriesIndex = options.index; + } + break; + } + } + }; + + this.drawHit = function (options) { + var + context = options.context, + args = options.args, + data = options.data, + xScale = options.xScale, + index = args.index, + x = xScale(args.x), + y = options.yScale(args.y), + x2; + + if (data.length - 1 > index) { + x2 = options.xScale(data[index + 1][0]); + context.save(); + context.strokeStyle = options.color; + context.lineWidth = options.lineWidth; + context.beginPath(); + context.moveTo(x, y); + context.lineTo(x2, y); + context.stroke(); + context.closePath(); + context.restore(); + } + }; + + this.clearHit = function (options) { + var + context = options.context, + args = options.args, + data = options.data, + xScale = options.xScale, + width = options.lineWidth, + index = args.index, + x = xScale(args.x), + y = options.yScale(args.y), + x2; + + if (data.length - 1 > index) { + x2 = options.xScale(data[index + 1][0]); + context.clearRect(x - width, y - width, x2 - x + 2 * width, 2 * width); + } + }; + } + } + +}); + +/** Bars **/ +Flotr.addType('bars', { + + options: { + show: false, // => setting to true will show bars, false will hide + lineWidth: 2, // => in pixels + barWidth: 1, // => in units of the x axis + fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill + fillColor: null, // => fill color + fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill + horizontal: false, // => horizontal bars (x and y inverted) + stacked: false, // => stacked bar charts + centered: true, // => center the bars to their x axis value + topPadding: 0.1, // => top padding in percent + grouped: false // => groups bars together which share x value, hit not supported. + }, + + stack : { + positive : [], + negative : [], + _positive : [], // Shadow + _negative : [] // Shadow + }, + + draw : function (options) { + var + context = options.context; + + this.current += 1; + + context.save(); + context.lineJoin = 'miter'; + // @TODO linewidth not interpreted the right way. + context.lineWidth = options.lineWidth; + context.strokeStyle = options.color; + if (options.fill) context.fillStyle = options.fillStyle; + + this.plot(options); + + context.restore(); + }, + + plot : function (options) { + + var + data = options.data, + context = options.context, + shadowSize = options.shadowSize, + i, geometry, left, top, width, height; + + if (data.length < 1) return; + + this.translate(context, options.horizontal); + + for (i = 0; i < data.length; i++) { + + geometry = this.getBarGeometry(data[i][0], data[i][1], options); + if (geometry === null) continue; + + left = geometry.left; + top = geometry.top; + width = geometry.width; + height = geometry.height; + + if (options.fill) context.fillRect(left, top, width, height); + if (shadowSize) { + context.save(); + context.fillStyle = 'rgba(0,0,0,0.05)'; + context.fillRect(left + shadowSize, top + shadowSize, width, height); + context.restore(); + } + if (options.lineWidth) { + context.strokeRect(left, top, width, height); + } + } + }, + + translate : function (context, horizontal) { + if (horizontal) { + context.rotate(-Math.PI / 2); + context.scale(-1, 1); + } + }, + + getBarGeometry : function (x, y, options) { + + var + horizontal = options.horizontal, + barWidth = options.barWidth, + centered = options.centered, + stack = options.stacked ? this.stack : false, + lineWidth = options.lineWidth, + bisection = centered ? barWidth / 2 : 0, + xScale = horizontal ? options.yScale : options.xScale, + yScale = horizontal ? options.xScale : options.yScale, + xValue = horizontal ? y : x, + yValue = horizontal ? x : y, + stackOffset = 0, + stackValue, left, right, top, bottom; + + if (options.grouped) { + this.current / this.groups; + xValue = xValue - bisection; + barWidth = barWidth / this.groups; + bisection = barWidth / 2; + xValue = xValue + barWidth * this.current - bisection; + } + + // Stacked bars + if (stack) { + stackValue = yValue > 0 ? stack.positive : stack.negative; + stackOffset = stackValue[xValue] || stackOffset; + stackValue[xValue] = stackOffset + yValue; + } + + left = xScale(xValue - bisection); + right = xScale(xValue + barWidth - bisection); + top = yScale(yValue + stackOffset); + bottom = yScale(stackOffset); + + // TODO for test passing... probably looks better without this + if (bottom < 0) bottom = 0; + + // TODO Skipping... + // if (right < xa.min || left > xa.max || top < ya.min || bottom > ya.max) continue; + + return (x === null || y === null) ? null : { + x : xValue, + y : yValue, + xScale : xScale, + yScale : yScale, + top : top, + left : Math.min(left, right) - lineWidth / 2, + width : Math.abs(right - left) - lineWidth, + height : bottom - top + }; + }, + + hit : function (options) { + var + data = options.data, + args = options.args, + mouse = args[0], + n = args[1], + x = options.xInverse(mouse.relX), + y = options.yInverse(mouse.relY), + hitGeometry = this.getBarGeometry(x, y, options), + width = hitGeometry.width / 2, + left = hitGeometry.left, + height = hitGeometry.y, + geometry, i; + + for (i = data.length; i--;) { + geometry = this.getBarGeometry(data[i][0], data[i][1], options); + if ( + // Height: + ( + // Positive Bars: + (height > 0 && height < geometry.y) || + // Negative Bars: + (height < 0 && height > geometry.y) + ) && + // Width: + (Math.abs(left - geometry.left) < width) + ) { + n.x = data[i][0]; + n.y = data[i][1]; + n.index = i; + n.seriesIndex = options.index; + } + } + }, + + drawHit : function (options) { + // TODO hits for stacked bars; implement using calculateStack option? + var + context = options.context, + args = options.args, + geometry = this.getBarGeometry(args.x, args.y, options), + left = geometry.left, + top = geometry.top, + width = geometry.width, + height = geometry.height; + + context.save(); + context.strokeStyle = options.color; + context.lineWidth = options.lineWidth; + this.translate(context, options.horizontal); + + // Draw highlight + context.beginPath(); + context.moveTo(left, top + height); + context.lineTo(left, top); + context.lineTo(left + width, top); + context.lineTo(left + width, top + height); + if (options.fill) { + context.fillStyle = options.fillStyle; + context.fill(); + } + context.stroke(); + context.closePath(); + + context.restore(); + }, + + clearHit: function (options) { + var + context = options.context, + args = options.args, + geometry = this.getBarGeometry(args.x, args.y, options), + left = geometry.left, + width = geometry.width, + top = geometry.top, + height = geometry.height, + lineWidth = 2 * options.lineWidth; + + context.save(); + this.translate(context, options.horizontal); + context.clearRect( + left - lineWidth, + Math.min(top, top + height) - lineWidth, + width + 2 * lineWidth, + Math.abs(height) + 2 * lineWidth + ); + context.restore(); + }, + + extendXRange : function (axis, data, options, bars) { + this._extendRange(axis, data, options, bars); + this.groups = (this.groups + 1) || 1; + this.current = 0; + }, + + extendYRange : function (axis, data, options, bars) { + this._extendRange(axis, data, options, bars); + }, + _extendRange: function (axis, data, options, bars) { + + var + max = axis.options.max; + + if (_.isNumber(max) || _.isString(max)) return; + + var + newmin = axis.min, + newmax = axis.max, + horizontal = options.horizontal, + orientation = axis.orientation, + positiveSums = this.positiveSums || {}, + negativeSums = this.negativeSums || {}, + value, datum, index, j; + + // Sides of bars + if ((orientation == 1 && !horizontal) || (orientation == -1 && horizontal)) { + if (options.centered) { + newmax = Math.max(axis.datamax + options.barWidth, newmax); + newmin = Math.min(axis.datamin - options.barWidth, newmin); + } + } + + if (options.stacked && + ((orientation == 1 && horizontal) || (orientation == -1 && !horizontal))){ + + for (j = data.length; j--;) { + value = data[j][(orientation == 1 ? 1 : 0)]+''; + datum = data[j][(orientation == 1 ? 0 : 1)]; + + // Positive + if (datum > 0) { + positiveSums[value] = (positiveSums[value] || 0) + datum; + newmax = Math.max(newmax, positiveSums[value]); + } + + // Negative + else { + negativeSums[value] = (negativeSums[value] || 0) + datum; + newmin = Math.min(newmin, negativeSums[value]); + } + } + } + + // End of bars + if ((orientation == 1 && horizontal) || (orientation == -1 && !horizontal)) { + if (options.topPadding && (axis.max === axis.datamax || (options.stacked && this.stackMax !== newmax))) { + newmax += options.topPadding * (newmax - newmin); + } + } + + this.stackMin = newmin; + this.stackMax = newmax; + this.negativeSums = negativeSums; + this.positiveSums = positiveSums; + + axis.max = newmax; + axis.min = newmin; + } + +}); + +/** Bubbles **/ +Flotr.addType('bubbles', { + options: { + show: false, // => setting to true will show radar chart, false will hide + lineWidth: 2, // => line width in pixels + fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill + fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill + baseRadius: 2 // => ratio of the radar, against the plot size + }, + draw : function (options) { + var + context = options.context, + shadowSize = options.shadowSize; + + context.save(); + context.lineWidth = options.lineWidth; + + // Shadows + context.fillStyle = 'rgba(0,0,0,0.05)'; + context.strokeStyle = 'rgba(0,0,0,0.05)'; + this.plot(options, shadowSize / 2); + context.strokeStyle = 'rgba(0,0,0,0.1)'; + this.plot(options, shadowSize / 4); + + // Chart + context.strokeStyle = options.color; + context.fillStyle = options.fillStyle; + this.plot(options); + + context.restore(); + }, + plot : function (options, offset) { + + var + data = options.data, + context = options.context, + geometry, + i, x, y, z; + + offset = offset || 0; + + for (i = 0; i < data.length; ++i){ + + geometry = this.getGeometry(data[i], options); + + context.beginPath(); + context.arc(geometry.x + offset, geometry.y + offset, geometry.z, 0, 2 * Math.PI, true); + context.stroke(); + if (options.fill) context.fill(); + context.closePath(); + } + }, + getGeometry : function (point, options) { + return { + x : options.xScale(point[0]), + y : options.yScale(point[1]), + z : point[2] * options.baseRadius + }; + }, + hit : function (options) { + var + data = options.data, + args = options.args, + mouse = args[0], + n = args[1], + relX = mouse.relX, + relY = mouse.relY, + distance, + geometry, + dx, dy; + + n.best = n.best || Number.MAX_VALUE; + + for (i = data.length; i--;) { + geometry = this.getGeometry(data[i], options); + + dx = geometry.x - relX; + dy = geometry.y - relY; + distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < geometry.z && geometry.z < n.best) { + n.x = data[i][0]; + n.y = data[i][1]; + n.index = i; + n.seriesIndex = options.index; + n.best = geometry.z; + } + } + }, + drawHit : function (options) { + + var + context = options.context, + geometry = this.getGeometry(options.data[options.args.index], options); + + context.save(); + context.lineWidth = options.lineWidth; + context.fillStyle = options.fillStyle; + context.strokeStyle = options.color; + context.beginPath(); + context.arc(geometry.x, geometry.y, geometry.z, 0, 2 * Math.PI, true); + context.fill(); + context.stroke(); + context.closePath(); + context.restore(); + }, + clearHit : function (options) { + + var + context = options.context, + geometry = this.getGeometry(options.data[options.args.index], options), + offset = geometry.z + options.lineWidth; + + context.save(); + context.clearRect( + geometry.x - offset, + geometry.y - offset, + 2 * offset, + 2 * offset + ); + context.restore(); + } + // TODO Add a hit calculation method (like pie) +}); + +/** Candles **/ +Flotr.addType('candles', { + options: { + show: false, // => setting to true will show candle sticks, false will hide + lineWidth: 1, // => in pixels + wickLineWidth: 1, // => in pixels + candleWidth: 0.6, // => in units of the x axis + fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill + upFillColor: '#00A8F0',// => up sticks fill color + downFillColor: '#CB4B4B',// => down sticks fill color + fillOpacity: 0.5, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill + // TODO Test this barcharts option. + barcharts: false // => draw as barcharts (not standard bars but financial barcharts) + }, + + draw : function (options) { + + var + context = options.context; + + context.save(); + context.lineJoin = 'miter'; + context.lineCap = 'butt'; + // @TODO linewidth not interpreted the right way. + context.lineWidth = options.wickLineWidth || options.lineWidth; + + this.plot(options); + + context.restore(); + }, + + plot : function (options) { + + var + data = options.data, + context = options.context, + xScale = options.xScale, + yScale = options.yScale, + width = options.candleWidth / 2, + shadowSize = options.shadowSize, + lineWidth = options.lineWidth, + wickLineWidth = options.wickLineWidth, + pixelOffset = (wickLineWidth % 2) / 2, + color, + datum, x, y, + open, high, low, close, + left, right, bottom, top, bottom2, top2, + i; + + if (data.length < 1) return; + + for (i = 0; i < data.length; i++) { + datum = data[i]; + x = datum[0]; + open = datum[1]; + high = datum[2]; + low = datum[3]; + close = datum[4]; + left = xScale(x - width); + right = xScale(x + width); + bottom = yScale(low); + top = yScale(high); + bottom2 = yScale(Math.min(open, close)); + top2 = yScale(Math.max(open, close)); + + /* + // TODO skipping + if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max) + continue; + */ + + color = options[open > close ? 'downFillColor' : 'upFillColor']; + + // Fill the candle. + // TODO Test the barcharts option + if (options.fill && !options.barcharts) { + context.fillStyle = 'rgba(0,0,0,0.05)'; + context.fillRect(left + shadowSize, top2 + shadowSize, right - left, bottom2 - top2); + context.save(); + context.globalAlpha = options.fillOpacity; + context.fillStyle = color; + context.fillRect(left, top2 + lineWidth, right - left, bottom2 - top2); + context.restore(); + } + + // Draw candle outline/border, high, low. + if (lineWidth || wickLineWidth) { + + x = Math.floor((left + right) / 2) + pixelOffset; + + context.strokeStyle = color; + context.beginPath(); + + // TODO Again with the bartcharts + if (options.barcharts) { + + context.moveTo(x, Math.floor(top + width)); + context.lineTo(x, Math.floor(bottom + width)); + + y = Math.floor(open + width) + 0.5; + context.moveTo(Math.floor(left) + pixelOffset, y); + context.lineTo(x, y); + + y = Math.floor(close + width) + 0.5; + context.moveTo(Math.floor(right) + pixelOffset, y); + context.lineTo(x, y); + } else { + context.strokeRect(left, top2 + lineWidth, right - left, bottom2 - top2); + + context.moveTo(x, Math.floor(top2 + lineWidth)); + context.lineTo(x, Math.floor(top + lineWidth)); + context.moveTo(x, Math.floor(bottom2 + lineWidth)); + context.lineTo(x, Math.floor(bottom + lineWidth)); + } + + context.closePath(); + context.stroke(); + } + } + }, + extendXRange: function (axis, data, options) { + if (axis.options.max === null) { + axis.max = Math.max(axis.datamax + 0.5, axis.max); + axis.min = Math.min(axis.datamin - 0.5, axis.min); + } + } +}); + +/** Gantt + * Base on data in form [s,y,d] where: + * y - executor or simply y value + * s - task start value + * d - task duration + * **/ +Flotr.addType('gantt', { + options: { + show: false, // => setting to true will show gantt, false will hide + lineWidth: 2, // => in pixels + barWidth: 1, // => in units of the x axis + fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill + fillColor: null, // => fill color + fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill + centered: true // => center the bars to their x axis value + }, + /** + * Draws gantt series in the canvas element. + * @param {Object} series - Series with options.gantt.show = true. + */ + draw: function(series) { + var ctx = this.ctx, + bw = series.gantt.barWidth, + lw = Math.min(series.gantt.lineWidth, bw); + + ctx.save(); + ctx.translate(this.plotOffset.left, this.plotOffset.top); + ctx.lineJoin = 'miter'; + + /** + * @todo linewidth not interpreted the right way. + */ + ctx.lineWidth = lw; + ctx.strokeStyle = series.color; + + ctx.save(); + this.gantt.plotShadows(series, bw, 0, series.gantt.fill); + ctx.restore(); + + if(series.gantt.fill){ + var color = series.gantt.fillColor || series.color; + ctx.fillStyle = this.processColor(color, {opacity: series.gantt.fillOpacity}); + } + + this.gantt.plot(series, bw, 0, series.gantt.fill); + ctx.restore(); + }, + plot: function(series, barWidth, offset, fill){ + var data = series.data; + if(data.length < 1) return; + + var xa = series.xaxis, + ya = series.yaxis, + ctx = this.ctx, i; + + for(i = 0; i < data.length; i++){ + var y = data[i][0], + s = data[i][1], + d = data[i][2], + drawLeft = true, drawTop = true, drawRight = true; + + if (s === null || d === null) continue; + + var left = s, + right = s + d, + bottom = y - (series.gantt.centered ? barWidth/2 : 0), + top = y + barWidth - (series.gantt.centered ? barWidth/2 : 0); + + if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max) + continue; + + if(left < xa.min){ + left = xa.min; + drawLeft = false; + } + + if(right > xa.max){ + right = xa.max; + if (xa.lastSerie != series) + drawTop = false; + } + + if(bottom < ya.min) + bottom = ya.min; + + if(top > ya.max){ + top = ya.max; + if (ya.lastSerie != series) + drawTop = false; + } + + /** + * Fill the bar. + */ + if(fill){ + ctx.beginPath(); + ctx.moveTo(xa.d2p(left), ya.d2p(bottom) + offset); + ctx.lineTo(xa.d2p(left), ya.d2p(top) + offset); + ctx.lineTo(xa.d2p(right), ya.d2p(top) + offset); + ctx.lineTo(xa.d2p(right), ya.d2p(bottom) + offset); + ctx.fill(); + ctx.closePath(); + } + + /** + * Draw bar outline/border. + */ + if(series.gantt.lineWidth && (drawLeft || drawRight || drawTop)){ + ctx.beginPath(); + ctx.moveTo(xa.d2p(left), ya.d2p(bottom) + offset); + + ctx[drawLeft ?'lineTo':'moveTo'](xa.d2p(left), ya.d2p(top) + offset); + ctx[drawTop ?'lineTo':'moveTo'](xa.d2p(right), ya.d2p(top) + offset); + ctx[drawRight?'lineTo':'moveTo'](xa.d2p(right), ya.d2p(bottom) + offset); + + ctx.stroke(); + ctx.closePath(); + } + } + }, + plotShadows: function(series, barWidth, offset){ + var data = series.data; + if(data.length < 1) return; + + var i, y, s, d, + xa = series.xaxis, + ya = series.yaxis, + ctx = this.ctx, + sw = this.options.shadowSize; + + for(i = 0; i < data.length; i++){ + y = data[i][0]; + s = data[i][1]; + d = data[i][2]; + + if (s === null || d === null) continue; + + var left = s, + right = s + d, + bottom = y - (series.gantt.centered ? barWidth/2 : 0), + top = y + barWidth - (series.gantt.centered ? barWidth/2 : 0); + + if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max) + continue; + + if(left < xa.min) left = xa.min; + if(right > xa.max) right = xa.max; + if(bottom < ya.min) bottom = ya.min; + if(top > ya.max) top = ya.max; + + var width = xa.d2p(right)-xa.d2p(left)-((xa.d2p(right)+sw <= this.plotWidth) ? 0 : sw); + var height = ya.d2p(bottom)-ya.d2p(top)-((ya.d2p(bottom)+sw <= this.plotHeight) ? 0 : sw ); + + ctx.fillStyle = 'rgba(0,0,0,0.05)'; + ctx.fillRect(Math.min(xa.d2p(left)+sw, this.plotWidth), Math.min(ya.d2p(top)+sw, this.plotHeight), width, height); + } + }, + extendXRange: function(axis) { + if(axis.options.max === null){ + var newmin = axis.min, + newmax = axis.max, + i, j, x, s, g, + stackedSumsPos = {}, + stackedSumsNeg = {}, + lastSerie = null; + + for(i = 0; i < this.series.length; ++i){ + s = this.series[i]; + g = s.gantt; + + if(g.show && s.xaxis == axis) { + for (j = 0; j < s.data.length; j++) { + if (g.show) { + y = s.data[j][0]+''; + stackedSumsPos[y] = Math.max((stackedSumsPos[y] || 0), s.data[j][1]+s.data[j][2]); + lastSerie = s; + } + } + for (j in stackedSumsPos) { + newmax = Math.max(stackedSumsPos[j], newmax); + } + } + } + axis.lastSerie = lastSerie; + axis.max = newmax; + axis.min = newmin; + } + }, + extendYRange: function(axis){ + if(axis.options.max === null){ + var newmax = Number.MIN_VALUE, + newmin = Number.MAX_VALUE, + i, j, s, g, + stackedSumsPos = {}, + stackedSumsNeg = {}, + lastSerie = null; + + for(i = 0; i < this.series.length; ++i){ + s = this.series[i]; + g = s.gantt; + + if (g.show && !s.hide && s.yaxis == axis) { + var datamax = Number.MIN_VALUE, datamin = Number.MAX_VALUE; + for(j=0; j < s.data.length; j++){ + datamax = Math.max(datamax,s.data[j][0]); + datamin = Math.min(datamin,s.data[j][0]); + } + + if (g.centered) { + newmax = Math.max(datamax + 0.5, newmax); + newmin = Math.min(datamin - 0.5, newmin); + } + else { + newmax = Math.max(datamax + 1, newmax); + newmin = Math.min(datamin, newmin); + } + // For normal horizontal bars + if (g.barWidth + datamax > newmax){ + newmax = axis.max + g.barWidth; + } + } + } + axis.lastSerie = lastSerie; + axis.max = newmax; + axis.min = newmin; + axis.tickSize = Flotr.getTickSize(axis.options.noTicks, newmin, newmax, axis.options.tickDecimals); + } + } +}); + +/** Markers **/ +/** + * Formats the marker labels. + * @param {Object} obj - Marker value Object {x:..,y:..} + * @return {String} Formatted marker string + */ +(function () { + +Flotr.defaultMarkerFormatter = function(obj){ + return (Math.round(obj.y*100)/100)+''; +}; + +Flotr.addType('markers', { + options: { + show: false, // => setting to true will show markers, false will hide + lineWidth: 1, // => line width of the rectangle around the marker + color: '#000000', // => text color + fill: false, // => fill or not the marekers' rectangles + fillColor: "#FFFFFF", // => fill color + fillOpacity: 0.4, // => fill opacity + stroke: false, // => draw the rectangle around the markers + position: 'ct', // => the markers position (vertical align: b, m, t, horizontal align: l, c, r) + verticalMargin: 0, // => the margin between the point and the text. + labelFormatter: Flotr.defaultMarkerFormatter, + fontSize: Flotr.defaultOptions.fontSize, + stacked: false, // => true if markers should be stacked + stackingType: 'b', // => define staching behavior, (b- bars like, a - area like) (see Issue 125 for details) + horizontal: false // => true if markers should be horizontal (For now only in a case on horizontal stacked bars, stacks should be calculated horizontaly) + }, + + // TODO test stacked markers. + stack : { + positive : [], + negative : [], + values : [] + }, + + draw : function (options) { + + var + data = options.data, + context = options.context, + stack = options.stacked ? options.stack : false, + stackType = options.stackingType, + stackOffsetNeg, + stackOffsetPos, + stackOffset, + i, x, y, label; + + context.save(); + context.lineJoin = 'round'; + context.lineWidth = options.lineWidth; + context.strokeStyle = 'rgba(0,0,0,0.5)'; + context.fillStyle = options.fillStyle; + + function stackPos (a, b) { + stackOffsetPos = stack.negative[a] || 0; + stackOffsetNeg = stack.positive[a] || 0; + if (b > 0) { + stack.positive[a] = stackOffsetPos + b; + return stackOffsetPos + b; + } else { + stack.negative[a] = stackOffsetNeg + b; + return stackOffsetNeg + b; + } + } + + for (i = 0; i < data.length; ++i) { + + x = data[i][0]; + y = data[i][1]; + + if (stack) { + if (stackType == 'b') { + if (options.horizontal) y = stackPos(y, x); + else x = stackPos(x, y); + } else if (stackType == 'a') { + stackOffset = stack.values[x] || 0; + stack.values[x] = stackOffset + y; + y = stackOffset + y; + } + } + + label = options.labelFormatter({x: x, y: y, index: i, data : data}); + this.plot(options.xScale(x), options.yScale(y), label, options); + } + context.restore(); + }, + plot: function(x, y, label, options) { + var context = options.context; + if (isImage(label) && !label.complete) { + throw 'Marker image not loaded.'; + } else { + this._plot(x, y, label, options); + } + }, + + _plot: function(x, y, label, options) { + var context = options.context, + margin = 2, + left = x, + top = y, + dim; + + if (isImage(label)) + dim = {height : label.height, width: label.width}; + else + dim = options.text.canvas(label); + + dim.width = Math.floor(dim.width+margin*2); + dim.height = Math.floor(dim.height+margin*2); + + if (options.position.indexOf('c') != -1) left -= dim.width/2 + margin; + else if (options.position.indexOf('l') != -1) left -= dim.width; + + if (options.position.indexOf('m') != -1) top -= dim.height/2 + margin; + else if (options.position.indexOf('t') != -1) top -= dim.height + options.verticalMargin; + else top += options.verticalMargin; + + left = Math.floor(left)+0.5; + top = Math.floor(top)+0.5; + + if(options.fill) + context.fillRect(left, top, dim.width, dim.height); + + if(options.stroke) + context.strokeRect(left, top, dim.width, dim.height); + + if (isImage(label)) + context.drawImage(label, left+margin, top+margin); + else + Flotr.drawText(context, label, left+margin, top+margin, {textBaseline: 'top', textAlign: 'left', size: options.fontSize, color: options.color}); + } +}); + +function isImage (i) { + return typeof i === 'object' && i.constructor && (Image ? true : i.constructor === Image); +} + +})(); + +/** + * Pie + * + * Formats the pies labels. + * @param {Object} slice - Slice object + * @return {String} Formatted pie label string + */ +(function () { + +var + _ = Flotr._; + +Flotr.defaultPieLabelFormatter = function (total, value) { + return (100 * value / total).toFixed(2)+'%'; +}; + +Flotr.addType('pie', { + options: { + show: false, // => setting to true will show bars, false will hide + lineWidth: 1, // => in pixels + fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill + fillColor: null, // => fill color + fillOpacity: 0.6, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill + explode: 6, // => the number of pixels the splices will be far from the center + sizeRatio: 0.6, // => the size ratio of the pie relative to the plot + startAngle: Math.PI/4, // => the first slice start angle + labelFormatter: Flotr.defaultPieLabelFormatter, + pie3D: false, // => whether to draw the pie in 3 dimenstions or not (ineffective) + pie3DviewAngle: (Math.PI/2 * 0.8), + pie3DspliceThickness: 20, + epsilon: 0.1 // => how close do you have to get to hit empty slice + }, + + draw : function (options) { + + // TODO 3D charts what? + + var + data = options.data, + context = options.context, + canvas = context.canvas, + lineWidth = options.lineWidth, + shadowSize = options.shadowSize, + sizeRatio = options.sizeRatio, + height = options.height, + width = options.width, + explode = options.explode, + color = options.color, + fill = options.fill, + fillStyle = options.fillStyle, + radius = Math.min(canvas.width, canvas.height) * sizeRatio / 2, + value = data[0][1], + html = [], + vScale = 1,//Math.cos(series.pie.viewAngle); + measure = Math.PI * 2 * value / this.total, + startAngle = this.startAngle || (2 * Math.PI * options.startAngle), // TODO: this initial startAngle is already in radians (fixing will be test-unstable) + endAngle = startAngle + measure, + bisection = startAngle + measure / 2, + label = options.labelFormatter(this.total, value), + //plotTickness = Math.sin(series.pie.viewAngle)*series.pie.spliceThickness / vScale; + explodeCoeff = explode + radius + 4, + distX = Math.cos(bisection) * explodeCoeff, + distY = Math.sin(bisection) * explodeCoeff, + textAlign = distX < 0 ? 'right' : 'left', + textBaseline = distY > 0 ? 'top' : 'bottom', + style, + x, y; + + context.save(); + context.translate(width / 2, height / 2); + context.scale(1, vScale); + + x = Math.cos(bisection) * explode; + y = Math.sin(bisection) * explode; + + // Shadows + if (shadowSize > 0) { + this.plotSlice(x + shadowSize, y + shadowSize, radius, startAngle, endAngle, context); + if (fill) { + context.fillStyle = 'rgba(0,0,0,0.1)'; + context.fill(); + } + } + + this.plotSlice(x, y, radius, startAngle, endAngle, context); + if (fill) { + context.fillStyle = fillStyle; + context.fill(); + } + context.lineWidth = lineWidth; + context.strokeStyle = color; + context.stroke(); + + style = { + size : options.fontSize * 1.2, + color : options.fontColor, + weight : 1.5 + }; + + if (label) { + if (options.htmlText || !options.textEnabled) { + divStyle = 'position:absolute;' + textBaseline + ':' + (height / 2 + (textBaseline === 'top' ? distY : -distY)) + 'px;'; + divStyle += textAlign + ':' + (width / 2 + (textAlign === 'right' ? -distX : distX)) + 'px;'; + html.push('
', label, '
'); + } + else { + style.textAlign = textAlign; + style.textBaseline = textBaseline; + Flotr.drawText(context, label, distX, distY, style); + } + } + + if (options.htmlText || !options.textEnabled) { + var div = Flotr.DOM.node('
'); + Flotr.DOM.insert(div, html.join('')); + Flotr.DOM.insert(options.element, div); + } + + context.restore(); + + // New start angle + this.startAngle = endAngle; + this.slices = this.slices || []; + this.slices.push({ + radius : Math.min(canvas.width, canvas.height) * sizeRatio / 2, + x : x, + y : y, + explode : explode, + start : startAngle, + end : endAngle + }); + }, + plotSlice : function (x, y, radius, startAngle, endAngle, context) { + context.beginPath(); + context.moveTo(x, y); + context.arc(x, y, radius, startAngle, endAngle, false); + context.lineTo(x, y); + context.closePath(); + }, + hit : function (options) { + + var + data = options.data[0], + args = options.args, + index = options.index, + mouse = args[0], + n = args[1], + slice = this.slices[index], + x = mouse.relX - options.width / 2, + y = mouse.relY - options.height / 2, + r = Math.sqrt(x * x + y * y), + theta = Math.atan(y / x), + circle = Math.PI * 2, + explode = slice.explode || options.explode, + start = slice.start % circle, + end = slice.end % circle, + epsilon = options.epsilon; + + if (x < 0) { + theta += Math.PI; + } else if (x > 0 && y < 0) { + theta += circle; + } + + if (r < slice.radius + explode && r > explode) { + if ( + (theta > start && theta < end) || // Normal Slice + (start > end && (theta < end || theta > start)) || // First slice + // TODO: Document the two cases at the end: + (start === end && ((slice.start === slice.end && Math.abs(theta - start) < epsilon) || (slice.start !== slice.end && Math.abs(theta-start) > epsilon))) + ) { + + // TODO Decouple this from hit plugin (chart shouldn't know what n means) + n.x = data[0]; + n.y = data[1]; + n.sAngle = start; + n.eAngle = end; + n.index = 0; + n.seriesIndex = index; + n.fraction = data[1] / this.total; + } + } + }, + drawHit: function (options) { + var + context = options.context, + slice = this.slices[options.args.seriesIndex]; + + context.save(); + context.translate(options.width / 2, options.height / 2); + this.plotSlice(slice.x, slice.y, slice.radius, slice.start, slice.end, context); + context.stroke(); + context.restore(); + }, + clearHit : function (options) { + var + context = options.context, + slice = this.slices[options.args.seriesIndex], + padding = 2 * options.lineWidth, + radius = slice.radius + padding; + + context.save(); + context.translate(options.width / 2, options.height / 2); + context.clearRect( + slice.x - radius, + slice.y - radius, + 2 * radius + padding, + 2 * radius + padding + ); + context.restore(); + }, + extendYRange : function (axis, data) { + this.total = (this.total || 0) + data[0][1]; + } +}); +})(); + +/** Points **/ +Flotr.addType('points', { + options: { + show: false, // => setting to true will show points, false will hide + radius: 3, // => point radius (pixels) + lineWidth: 2, // => line width in pixels + fill: true, // => true to fill the points with a color, false for (transparent) no fill + fillColor: '#FFFFFF', // => fill color. Null to use series color. + fillOpacity: 1, // => opacity of color inside the points + hitRadius: null // => override for points hit radius + }, + + draw : function (options) { + var + context = options.context, + lineWidth = options.lineWidth, + shadowSize = options.shadowSize; + + context.save(); + + if (shadowSize > 0) { + context.lineWidth = shadowSize / 2; + + context.strokeStyle = 'rgba(0,0,0,0.1)'; + this.plot(options, shadowSize / 2 + context.lineWidth / 2); + + context.strokeStyle = 'rgba(0,0,0,0.2)'; + this.plot(options, context.lineWidth / 2); + } + + context.lineWidth = options.lineWidth; + context.strokeStyle = options.color; + if (options.fill) context.fillStyle = options.fillStyle; + + this.plot(options); + context.restore(); + }, + + plot : function (options, offset) { + var + data = options.data, + context = options.context, + xScale = options.xScale, + yScale = options.yScale, + i, x, y; + + for (i = data.length - 1; i > -1; --i) { + y = data[i][1]; + if (y === null) continue; + + x = xScale(data[i][0]); + y = yScale(y); + + if (x < 0 || x > options.width || y < 0 || y > options.height) continue; + + context.beginPath(); + if (offset) { + context.arc(x, y + offset, options.radius, 0, Math.PI, false); + } else { + context.arc(x, y, options.radius, 0, 2 * Math.PI, true); + if (options.fill) context.fill(); + } + context.stroke(); + context.closePath(); + } + } +}); + +/** Radar **/ +Flotr.addType('radar', { + options: { + show: false, // => setting to true will show radar chart, false will hide + lineWidth: 2, // => line width in pixels + fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill + fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill + radiusRatio: 0.90 // => ratio of the radar, against the plot size + }, + draw : function (options) { + var + context = options.context, + shadowSize = options.shadowSize; + + context.save(); + context.translate(options.width / 2, options.height / 2); + context.lineWidth = options.lineWidth; + + // Shadow + context.fillStyle = 'rgba(0,0,0,0.05)'; + context.strokeStyle = 'rgba(0,0,0,0.05)'; + this.plot(options, shadowSize / 2); + context.strokeStyle = 'rgba(0,0,0,0.1)'; + this.plot(options, shadowSize / 4); + + // Chart + context.strokeStyle = options.color; + context.fillStyle = options.fillStyle; + this.plot(options); + + context.restore(); + }, + plot : function (options, offset) { + var + data = options.data, + context = options.context, + radius = Math.min(options.height, options.width) * options.radiusRatio / 2, + step = 2 * Math.PI / data.length, + angle = -Math.PI / 2, + i, ratio; + + offset = offset || 0; + + context.beginPath(); + for (i = 0; i < data.length; ++i) { + ratio = data[i][1] / this.max; + + context[i === 0 ? 'moveTo' : 'lineTo']( + Math.cos(i * step + angle) * radius * ratio + offset, + Math.sin(i * step + angle) * radius * ratio + offset + ); + } + context.closePath(); + if (options.fill) context.fill(); + context.stroke(); + }, + extendYRange : function (axis, data) { + this.max = Math.max(axis.max, this.max || -Number.MAX_VALUE); + } +}); + +Flotr.addType('timeline', { + options: { + show: false, + lineWidth: 1, + barWidth: 0.2, + fill: true, + fillColor: null, + fillOpacity: 0.4, + centered: true + }, + + draw : function (options) { + + var + context = options.context; + + context.save(); + context.lineJoin = 'miter'; + context.lineWidth = options.lineWidth; + context.strokeStyle = options.color; + context.fillStyle = options.fillStyle; + + this.plot(options); + + context.restore(); + }, + + plot : function (options) { + + var + data = options.data, + context = options.context, + xScale = options.xScale, + yScale = options.yScale, + barWidth = options.barWidth, + lineWidth = options.lineWidth, + i; + + Flotr._.each(data, function (timeline) { + + var + x = timeline[0], + y = timeline[1], + w = timeline[2], + h = barWidth, + + xt = Math.ceil(xScale(x)), + wt = Math.ceil(xScale(x + w)) - xt, + yt = Math.round(yScale(y)), + ht = Math.round(yScale(y - h)) - yt, + + x0 = xt - lineWidth / 2, + y0 = Math.round(yt - ht / 2) - lineWidth / 2; + + context.strokeRect(x0, y0, wt, ht); + context.fillRect(x0, y0, wt, ht); + + }); + }, + + extendRange : function (series) { + + var + data = series.data, + xa = series.xaxis, + ya = series.yaxis, + w = series.timeline.barWidth; + + if (xa.options.min === null) + xa.min = xa.datamin - w / 2; + + if (xa.options.max === null) { + + var + max = xa.max; + + Flotr._.each(data, function (timeline) { + max = Math.max(max, timeline[0] + timeline[2]); + }, this); + + xa.max = max + w / 2; + } + + if (ya.options.min === null) + ya.min = ya.datamin - w; + if (ya.options.min === null) + ya.max = ya.datamax + w; + } + +}); + +(function () { + +var D = Flotr.DOM; + +Flotr.addPlugin('crosshair', { + options: { + mode: null, // => one of null, 'x', 'y' or 'xy' + color: '#FF0000', // => crosshair color + hideCursor: true // => hide the cursor when the crosshair is shown + }, + callbacks: { + 'flotr:mousemove': function(e, pos) { + if (this.options.crosshair.mode) { + this.crosshair.clearCrosshair(); + this.crosshair.drawCrosshair(pos); + } + } + }, + /** + * Draws the selection box. + */ + drawCrosshair: function(pos) { + var octx = this.octx, + options = this.options.crosshair, + plotOffset = this.plotOffset, + x = plotOffset.left + Math.round(pos.relX) + .5, + y = plotOffset.top + Math.round(pos.relY) + .5; + + if (pos.relX < 0 || pos.relY < 0 || pos.relX > this.plotWidth || pos.relY > this.plotHeight) { + this.el.style.cursor = null; + D.removeClass(this.el, 'flotr-crosshair'); + return; + } + + if (options.hideCursor) { + this.el.style.cursor = 'none'; + D.addClass(this.el, 'flotr-crosshair'); + } + + octx.save(); + octx.strokeStyle = options.color; + octx.lineWidth = 1; + octx.beginPath(); + + if (options.mode.indexOf('x') != -1) { + octx.moveTo(x, plotOffset.top); + octx.lineTo(x, plotOffset.top + this.plotHeight); + } + + if (options.mode.indexOf('y') != -1) { + octx.moveTo(plotOffset.left, y); + octx.lineTo(plotOffset.left + this.plotWidth, y); + } + + octx.stroke(); + octx.restore(); + }, + /** + * Removes the selection box from the overlay canvas. + */ + clearCrosshair: function() { + + var + plotOffset = this.plotOffset, + position = this.lastMousePos, + context = this.octx; + + if (position) { + context.clearRect( + Math.round(position.relX) + plotOffset.left, + plotOffset.top, + 1, + this.plotHeight + 1 + ); + context.clearRect( + plotOffset.left, + Math.round(position.relY) + plotOffset.top, + this.plotWidth + 1, + 1 + ); + } + } +}); +})(); + +(function() { + +var + D = Flotr.DOM, + _ = Flotr._; + +function getImage (type, canvas, width, height) { + + // TODO add scaling for w / h + var + mime = 'image/'+type, + data = canvas.toDataURL(mime), + image = new Image(); + image.src = data; + return image; +} + +Flotr.addPlugin('download', { + + saveImage: function (type, width, height, replaceCanvas) { + var image = null; + if (Flotr.isIE && Flotr.isIE < 9) { + image = ''+this.canvas.firstChild.innerHTML+''; + return window.open().document.write(image); + } + + if (type !== 'jpeg' && type !== 'png') return; + + image = getImage(type, this.canvas, width, height); + + if (_.isElement(image) && replaceCanvas) { + this.download.restoreCanvas(); + D.hide(this.canvas); + D.hide(this.overlay); + D.setStyles({position: 'absolute'}); + D.insert(this.el, image); + this.saveImageElement = image; + } else { + return window.open(image.src); + } + }, + + restoreCanvas: function() { + D.show(this.canvas); + D.show(this.overlay); + if (this.saveImageElement) this.el.removeChild(this.saveImageElement); + this.saveImageElement = null; + } +}); + +})(); + +(function () { + +var E = Flotr.EventAdapter, + _ = Flotr._; + +Flotr.addPlugin('graphGrid', { + + callbacks: { + 'flotr:beforedraw' : function () { + this.graphGrid.drawGrid(); + }, + 'flotr:afterdraw' : function () { + this.graphGrid.drawOutline(); + } + }, + + drawGrid: function(){ + + var + ctx = this.ctx, + options = this.options, + grid = options.grid, + verticalLines = grid.verticalLines, + horizontalLines = grid.horizontalLines, + minorVerticalLines = grid.minorVerticalLines, + minorHorizontalLines = grid.minorHorizontalLines, + plotHeight = this.plotHeight, + plotWidth = this.plotWidth, + a, v, i, j; + + if(verticalLines || minorVerticalLines || + horizontalLines || minorHorizontalLines){ + E.fire(this.el, 'flotr:beforegrid', [this.axes.x, this.axes.y, options, this]); + } + ctx.save(); + ctx.lineWidth = 1; + ctx.strokeStyle = grid.tickColor; + + function circularHorizontalTicks (ticks) { + for(i = 0; i < ticks.length; ++i){ + var ratio = ticks[i].v / a.max; + for(j = 0; j <= sides; ++j){ + ctx[j === 0 ? 'moveTo' : 'lineTo']( + Math.cos(j*coeff+angle)*radius*ratio, + Math.sin(j*coeff+angle)*radius*ratio + ); + } + } + } + function drawGridLines (ticks, callback) { + _.each(_.pluck(ticks, 'v'), function(v){ + // Don't show lines on upper and lower bounds. + if ((v <= a.min || v >= a.max) || + (v == a.min || v == a.max) && grid.outlineWidth) + return; + callback(Math.floor(a.d2p(v)) + ctx.lineWidth/2); + }); + } + function drawVerticalLines (x) { + ctx.moveTo(x, 0); + ctx.lineTo(x, plotHeight); + } + function drawHorizontalLines (y) { + ctx.moveTo(0, y); + ctx.lineTo(plotWidth, y); + } + + if (grid.circular) { + ctx.translate(this.plotOffset.left+plotWidth/2, this.plotOffset.top+plotHeight/2); + var radius = Math.min(plotHeight, plotWidth)*options.radar.radiusRatio/2, + sides = this.axes.x.ticks.length, + coeff = 2*(Math.PI/sides), + angle = -Math.PI/2; + + // Draw grid lines in vertical direction. + ctx.beginPath(); + + a = this.axes.y; + + if(horizontalLines){ + circularHorizontalTicks(a.ticks); + } + if(minorHorizontalLines){ + circularHorizontalTicks(a.minorTicks); + } + + if(verticalLines){ + _.times(sides, function(i){ + ctx.moveTo(0, 0); + ctx.lineTo(Math.cos(i*coeff+angle)*radius, Math.sin(i*coeff+angle)*radius); + }); + } + ctx.stroke(); + } + else { + ctx.translate(this.plotOffset.left, this.plotOffset.top); + + // Draw grid background, if present in options. + if(grid.backgroundColor){ + ctx.fillStyle = this.processColor(grid.backgroundColor, {x1: 0, y1: 0, x2: plotWidth, y2: plotHeight}); + ctx.fillRect(0, 0, plotWidth, plotHeight); + } + + ctx.beginPath(); + + a = this.axes.x; + if (verticalLines) drawGridLines(a.ticks, drawVerticalLines); + if (minorVerticalLines) drawGridLines(a.minorTicks, drawVerticalLines); + + a = this.axes.y; + if (horizontalLines) drawGridLines(a.ticks, drawHorizontalLines); + if (minorHorizontalLines) drawGridLines(a.minorTicks, drawHorizontalLines); + + ctx.stroke(); + } + + ctx.restore(); + if(verticalLines || minorVerticalLines || + horizontalLines || minorHorizontalLines){ + E.fire(this.el, 'flotr:aftergrid', [this.axes.x, this.axes.y, options, this]); + } + }, + + drawOutline: function(){ + var + that = this, + options = that.options, + grid = options.grid, + outline = grid.outline, + ctx = that.ctx, + backgroundImage = grid.backgroundImage, + plotOffset = that.plotOffset, + leftOffset = plotOffset.left, + topOffset = plotOffset.top, + plotWidth = that.plotWidth, + plotHeight = that.plotHeight, + v, img, src, left, top, globalAlpha; + + if (!grid.outlineWidth) return; + + ctx.save(); + + if (grid.circular) { + ctx.translate(leftOffset + plotWidth / 2, topOffset + plotHeight / 2); + var radius = Math.min(plotHeight, plotWidth) * options.radar.radiusRatio / 2, + sides = this.axes.x.ticks.length, + coeff = 2*(Math.PI/sides), + angle = -Math.PI/2; + + // Draw axis/grid border. + ctx.beginPath(); + ctx.lineWidth = grid.outlineWidth; + ctx.strokeStyle = grid.color; + ctx.lineJoin = 'round'; + + for(i = 0; i <= sides; ++i){ + ctx[i === 0 ? 'moveTo' : 'lineTo'](Math.cos(i*coeff+angle)*radius, Math.sin(i*coeff+angle)*radius); + } + //ctx.arc(0, 0, radius, 0, Math.PI*2, true); + + ctx.stroke(); + } + else { + ctx.translate(leftOffset, topOffset); + + // Draw axis/grid border. + var lw = grid.outlineWidth, + orig = 0.5-lw+((lw+1)%2/2), + lineTo = 'lineTo', + moveTo = 'moveTo'; + ctx.lineWidth = lw; + ctx.strokeStyle = grid.color; + ctx.lineJoin = 'miter'; + ctx.beginPath(); + ctx.moveTo(orig, orig); + plotWidth = plotWidth - (lw / 2) % 1; + plotHeight = plotHeight + lw / 2; + ctx[outline.indexOf('n') !== -1 ? lineTo : moveTo](plotWidth, orig); + ctx[outline.indexOf('e') !== -1 ? lineTo : moveTo](plotWidth, plotHeight); + ctx[outline.indexOf('s') !== -1 ? lineTo : moveTo](orig, plotHeight); + ctx[outline.indexOf('w') !== -1 ? lineTo : moveTo](orig, orig); + ctx.stroke(); + ctx.closePath(); + } + + ctx.restore(); + + if (backgroundImage) { + + src = backgroundImage.src || backgroundImage; + left = (parseInt(backgroundImage.left, 10) || 0) + plotOffset.left; + top = (parseInt(backgroundImage.top, 10) || 0) + plotOffset.top; + img = new Image(); + + img.onload = function() { + ctx.save(); + if (backgroundImage.alpha) ctx.globalAlpha = backgroundImage.alpha; + ctx.globalCompositeOperation = 'destination-over'; + ctx.drawImage(img, 0, 0, img.width, img.height, left, top, plotWidth, plotHeight); + ctx.restore(); + }; + + img.src = src; + } + } +}); + +})(); + +(function () { + +var + D = Flotr.DOM, + _ = Flotr._, + flotr = Flotr, + S_MOUSETRACK = 'opacity:0.7;background-color:#000;color:#fff;display:none;position:absolute;padding:2px 8px;-moz-border-radius:4px;border-radius:4px;white-space:nowrap;'; + +Flotr.addPlugin('hit', { + callbacks: { + 'flotr:mousemove': function(e, pos) { + this.hit.track(pos); + }, + 'flotr:click': function(pos) { + var + hit = this.hit.track(pos); + _.defaults(pos, hit); + }, + 'flotr:mouseout': function() { + this.hit.clearHit(); + }, + 'flotr:destroy': function() { + this.mouseTrack = null; + } + }, + track : function (pos) { + if (this.options.mouse.track || _.any(this.series, function(s){return s.mouse && s.mouse.track;})) { + return this.hit.hit(pos); + } + }, + /** + * Try a method on a graph type. If the method exists, execute it. + * @param {Object} series + * @param {String} method Method name. + * @param {Array} args Arguments applied to method. + * @return executed successfully or failed. + */ + executeOnType: function(s, method, args){ + var + success = false, + options; + + if (!_.isArray(s)) s = [s]; + + function e(s, index) { + _.each(_.keys(flotr.graphTypes), function (type) { + if (s[type] && s[type].show && this[type][method]) { + options = this.getOptions(s, type); + + options.fill = !!s.mouse.fillColor; + options.fillStyle = this.processColor(s.mouse.fillColor || '#ffffff', {opacity: s.mouse.fillOpacity}); + options.color = s.mouse.lineColor; + options.context = this.octx; + options.index = index; + + if (args) options.args = args; + this[type][method].call(this[type], options); + success = true; + } + }, this); + } + _.each(s, e, this); + + return success; + }, + /** + * Updates the mouse tracking point on the overlay. + */ + drawHit: function(n){ + var octx = this.octx, + s = n.series; + + if (s.mouse.lineColor) { + octx.save(); + octx.lineWidth = (s.points ? s.points.lineWidth : 1); + octx.strokeStyle = s.mouse.lineColor; + octx.fillStyle = this.processColor(s.mouse.fillColor || '#ffffff', {opacity: s.mouse.fillOpacity}); + octx.translate(this.plotOffset.left, this.plotOffset.top); + + if (!this.hit.executeOnType(s, 'drawHit', n)) { + var + xa = n.xaxis, + ya = n.yaxis; + + octx.beginPath(); + // TODO fix this (points) should move to general testable graph mixin + octx.arc(xa.d2p(n.x), ya.d2p(n.y), s.points.hitRadius || s.points.radius || s.mouse.radius, 0, 2 * Math.PI, true); + octx.fill(); + octx.stroke(); + octx.closePath(); + } + octx.restore(); + this.clip(octx); + } + this.prevHit = n; + }, + /** + * Removes the mouse tracking point from the overlay. + */ + clearHit: function(){ + var prev = this.prevHit, + octx = this.octx, + plotOffset = this.plotOffset; + octx.save(); + octx.translate(plotOffset.left, plotOffset.top); + if (prev) { + if (!this.hit.executeOnType(prev.series, 'clearHit', this.prevHit)) { + // TODO fix this (points) should move to general testable graph mixin + var + s = prev.series, + lw = (s.points ? s.points.lineWidth : 1); + offset = (s.points.hitRadius || s.points.radius || s.mouse.radius) + lw; + octx.clearRect( + prev.xaxis.d2p(prev.x) - offset, + prev.yaxis.d2p(prev.y) - offset, + offset*2, + offset*2 + ); + } + D.hide(this.mouseTrack); + this.prevHit = null; + } + octx.restore(); + }, + /** + * Retrieves the nearest data point from the mouse cursor. If it's within + * a certain range, draw a point on the overlay canvas and display the x and y + * value of the data. + * @param {Object} mouse - Object that holds the relative x and y coordinates of the cursor. + */ + hit : function (mouse) { + + var + options = this.options, + prevHit = this.prevHit, + closest, sensibility, dataIndex, seriesIndex, series, value, xaxis, yaxis, n; + + if (this.series.length === 0) return; + + // Nearest data element. + // dist, x, y, relX, relY, absX, absY, sAngle, eAngle, fraction, mouse, + // xaxis, yaxis, series, index, seriesIndex + n = { + relX : mouse.relX, + relY : mouse.relY, + absX : mouse.absX, + absY : mouse.absY + }; + + if (options.mouse.trackY && + !options.mouse.trackAll && + this.hit.executeOnType(this.series, 'hit', [mouse, n]) && + !_.isUndefined(n.seriesIndex)) + { + series = this.series[n.seriesIndex]; + n.series = series; + n.mouse = series.mouse; + n.xaxis = series.xaxis; + n.yaxis = series.yaxis; + } else { + + closest = this.hit.closest(mouse); + + if (closest) { + + closest = options.mouse.trackY ? closest.point : closest.x; + seriesIndex = closest.seriesIndex; + series = this.series[seriesIndex]; + xaxis = series.xaxis; + yaxis = series.yaxis; + sensibility = 2 * series.mouse.sensibility; + + if + (options.mouse.trackAll || + (closest.distanceX < sensibility / xaxis.scale && + (!options.mouse.trackY || closest.distanceY < sensibility / yaxis.scale))) + { + n.series = series; + n.xaxis = series.xaxis; + n.yaxis = series.yaxis; + n.mouse = series.mouse; + n.x = closest.x; + n.y = closest.y; + n.dist = closest.distance; + n.index = closest.dataIndex; + n.seriesIndex = seriesIndex; + } + } + } + + if (!prevHit || (prevHit.index !== n.index || prevHit.seriesIndex !== n.seriesIndex)) { + this.hit.clearHit(); + if (n.series && n.mouse && n.mouse.track) { + this.hit.drawMouseTrack(n); + this.hit.drawHit(n); + Flotr.EventAdapter.fire(this.el, 'flotr:hit', [n, this]); + } + } + + return n; + }, + + closest : function (mouse) { + + var + series = this.series, + options = this.options, + relX = mouse.relX, + relY = mouse.relY, + compare = Number.MAX_VALUE, + compareX = Number.MAX_VALUE, + closest = {}, + closestX = {}, + check = false, + serie, data, + distance, distanceX, distanceY, + mouseX, mouseY, + x, y, i, j; + + function setClosest (o) { + o.distance = distance; + o.distanceX = distanceX; + o.distanceY = distanceY; + o.seriesIndex = i; + o.dataIndex = j; + o.x = x; + o.y = y; + check = true; + } + + for (i = 0; i < series.length; i++) { + + serie = series[i]; + data = serie.data; + mouseX = serie.xaxis.p2d(relX); + mouseY = serie.yaxis.p2d(relY); + + for (j = data.length; j--;) { + + x = data[j][0]; + y = data[j][1]; + + if (x === null || y === null) continue; + + // don't check if the point isn't visible in the current range + if (x < serie.xaxis.min || x > serie.xaxis.max) continue; + + distanceX = Math.abs(x - mouseX); + distanceY = Math.abs(y - mouseY); + + // Skip square root for speed + distance = distanceX * distanceX + distanceY * distanceY; + + if (distance < compare) { + compare = distance; + setClosest(closest); + } + + if (distanceX < compareX) { + compareX = distanceX; + setClosest(closestX); + } + } + } + + return check ? { + point : closest, + x : closestX + } : false; + }, + + drawMouseTrack : function (n) { + + var + pos = '', + s = n.series, + p = n.mouse.position, + m = n.mouse.margin, + x = n.x, + y = n.y, + elStyle = S_MOUSETRACK, + mouseTrack = this.mouseTrack, + plotOffset = this.plotOffset, + left = plotOffset.left, + right = plotOffset.right, + bottom = plotOffset.bottom, + top = plotOffset.top, + decimals = n.mouse.trackDecimals, + options = this.options; + + // Create + if (!mouseTrack) { + mouseTrack = D.node('
'); + this.mouseTrack = mouseTrack; + D.insert(this.el, mouseTrack); + } + + if (!n.mouse.relative) { // absolute to the canvas + + if (p.charAt(0) == 'n') pos += 'top:' + (m + top) + 'px;bottom:auto;'; + else if (p.charAt(0) == 's') pos += 'bottom:' + (m + bottom) + 'px;top:auto;'; + if (p.charAt(1) == 'e') pos += 'right:' + (m + right) + 'px;left:auto;'; + else if (p.charAt(1) == 'w') pos += 'left:' + (m + left) + 'px;right:auto;'; + + // Pie + } else if (s.pie && s.pie.show) { + var center = { + x: (this.plotWidth)/2, + y: (this.plotHeight)/2 + }, + radius = (Math.min(this.canvasWidth, this.canvasHeight) * s.pie.sizeRatio) / 2, + bisection = n.sAngle one of null, 'x', 'y' or 'xy' + color: '#B6D9FF', // => selection box color + fps: 20 // => frames-per-second + }, + + callbacks: { + 'flotr:mouseup' : function (event) { + + var + options = this.options.selection, + selection = this.selection, + pointer = this.getEventPosition(event); + + if (!options || !options.mode) return; + if (selection.interval) clearInterval(selection.interval); + + if (this.multitouches) { + selection.updateSelection(); + } else + if (!options.pinchOnly) { + selection.setSelectionPos(selection.selection.second, pointer); + } + selection.clearSelection(); + + if(selection.selecting && selection.selectionIsSane()){ + selection.drawSelection(); + selection.fireSelectEvent(); + this.ignoreClick = true; + } + }, + 'flotr:mousedown' : function (event) { + + var + options = this.options.selection, + selection = this.selection, + pointer = this.getEventPosition(event); + + if (!options || !options.mode) return; + if (!options.mode || (!isLeftClick(event) && _.isUndefined(event.touches))) return; + if (!options.pinchOnly) selection.setSelectionPos(selection.selection.first, pointer); + if (selection.interval) clearInterval(selection.interval); + + this.lastMousePos.pageX = null; + selection.selecting = false; + selection.interval = setInterval( + _.bind(selection.updateSelection, this), + 1000 / options.fps + ); + }, + 'flotr:destroy' : function (event) { + clearInterval(this.selection.interval); + } + }, + + // TODO This isn't used. Maybe it belongs in the draw area and fire select event methods? + getArea: function() { + + var + s = this.selection.selection, + a = this.axes, + first = s.first, + second = s.second, + x1, x2, y1, y2; + + x1 = a.x.p2d(s.first.x); + x2 = a.x.p2d(s.second.x); + y1 = a.y.p2d(s.first.y); + y2 = a.y.p2d(s.second.y); + + return { + x1 : Math.min(x1, x2), + y1 : Math.min(y1, y2), + x2 : Math.max(x1, x2), + y2 : Math.max(y1, y2), + xfirst : x1, + xsecond : x2, + yfirst : y1, + ysecond : y2 + }; + }, + + selection: {first: {x: -1, y: -1}, second: {x: -1, y: -1}}, + prevSelection: null, + interval: null, + + /** + * Fires the 'flotr:select' event when the user made a selection. + */ + fireSelectEvent: function(name){ + var + area = this.selection.getArea(); + name = name || 'select'; + area.selection = this.selection.selection; + E.fire(this.el, 'flotr:'+name, [area, this]); + }, + + /** + * Allows the user the manually select an area. + * @param {Object} area - Object with coordinates to select. + */ + setSelection: function(area, preventEvent){ + var options = this.options, + xa = this.axes.x, + ya = this.axes.y, + vertScale = ya.scale, + hozScale = xa.scale, + selX = options.selection.mode.indexOf('x') != -1, + selY = options.selection.mode.indexOf('y') != -1, + s = this.selection.selection; + + this.selection.clearSelection(); + + s.first.y = boundY((selX && !selY) ? 0 : (ya.max - area.y1) * vertScale, this); + s.second.y = boundY((selX && !selY) ? this.plotHeight - 1: (ya.max - area.y2) * vertScale, this); + s.first.x = boundX((selY && !selX) ? 0 : (area.x1 - xa.min) * hozScale, this); + s.second.x = boundX((selY && !selX) ? this.plotWidth : (area.x2 - xa.min) * hozScale, this); + + this.selection.drawSelection(); + if (!preventEvent) + this.selection.fireSelectEvent(); + }, + + /** + * Calculates the position of the selection. + * @param {Object} pos - Position object. + * @param {Event} event - Event object. + */ + setSelectionPos: function(pos, pointer) { + var mode = this.options.selection.mode, + selection = this.selection.selection; + + if(mode.indexOf('x') == -1) { + pos.x = (pos == selection.first) ? 0 : this.plotWidth; + }else{ + pos.x = boundX(pointer.relX, this); + } + + if (mode.indexOf('y') == -1) { + pos.y = (pos == selection.first) ? 0 : this.plotHeight - 1; + }else{ + pos.y = boundY(pointer.relY, this); + } + }, + /** + * Draws the selection box. + */ + drawSelection: function() { + + this.selection.fireSelectEvent('selecting'); + + var s = this.selection.selection, + octx = this.octx, + options = this.options, + plotOffset = this.plotOffset, + prevSelection = this.selection.prevSelection; + + if (prevSelection && + s.first.x == prevSelection.first.x && + s.first.y == prevSelection.first.y && + s.second.x == prevSelection.second.x && + s.second.y == prevSelection.second.y) { + return; + } + + octx.save(); + octx.strokeStyle = this.processColor(options.selection.color, {opacity: 0.8}); + octx.lineWidth = 1; + octx.lineJoin = 'miter'; + octx.fillStyle = this.processColor(options.selection.color, {opacity: 0.4}); + + this.selection.prevSelection = { + first: { x: s.first.x, y: s.first.y }, + second: { x: s.second.x, y: s.second.y } + }; + + var x = Math.min(s.first.x, s.second.x), + y = Math.min(s.first.y, s.second.y), + w = Math.abs(s.second.x - s.first.x), + h = Math.abs(s.second.y - s.first.y); + + octx.fillRect(x + plotOffset.left+0.5, y + plotOffset.top+0.5, w, h); + octx.strokeRect(x + plotOffset.left+0.5, y + plotOffset.top+0.5, w, h); + octx.restore(); + }, + + /** + * Updates (draws) the selection box. + */ + updateSelection: function(){ + if (!this.lastMousePos.pageX) return; + + this.selection.selecting = true; + + if (this.multitouches) { + this.selection.setSelectionPos(this.selection.selection.first, this.getEventPosition(this.multitouches[0])); + this.selection.setSelectionPos(this.selection.selection.second, this.getEventPosition(this.multitouches[1])); + } else + if (this.options.selection.pinchOnly) { + return; + } else { + this.selection.setSelectionPos(this.selection.selection.second, this.lastMousePos); + } + + this.selection.clearSelection(); + + if(this.selection.selectionIsSane()) { + this.selection.drawSelection(); + } + }, + + /** + * Removes the selection box from the overlay canvas. + */ + clearSelection: function() { + if (!this.selection.prevSelection) return; + + var prevSelection = this.selection.prevSelection, + lw = 1, + plotOffset = this.plotOffset, + x = Math.min(prevSelection.first.x, prevSelection.second.x), + y = Math.min(prevSelection.first.y, prevSelection.second.y), + w = Math.abs(prevSelection.second.x - prevSelection.first.x), + h = Math.abs(prevSelection.second.y - prevSelection.first.y); + + this.octx.clearRect(x + plotOffset.left - lw + 0.5, + y + plotOffset.top - lw, + w + 2 * lw + 0.5, + h + 2 * lw + 0.5); + + this.selection.prevSelection = null; + }, + /** + * Determines whether or not the selection is sane and should be drawn. + * @return {Boolean} - True when sane, false otherwise. + */ + selectionIsSane: function(){ + var s = this.selection.selection; + return Math.abs(s.second.x - s.first.x) >= 5 || + Math.abs(s.second.y - s.first.y) >= 5; + } + +}); + +})(); + +(function () { + +var D = Flotr.DOM; + +Flotr.addPlugin('labels', { + + callbacks : { + 'flotr:afterdraw' : function () { + this.labels.draw(); + } + }, + + draw: function(){ + // Construct fixed width label boxes, which can be styled easily. + var + axis, tick, left, top, xBoxWidth, + radius, sides, coeff, angle, + div, i, html = '', + noLabels = 0, + options = this.options, + ctx = this.ctx, + a = this.axes, + style = { size: options.fontSize }; + + for (i = 0; i < a.x.ticks.length; ++i){ + if (a.x.ticks[i].label) { ++noLabels; } + } + xBoxWidth = this.plotWidth / noLabels; + + if (options.grid.circular) { + ctx.save(); + ctx.translate(this.plotOffset.left + this.plotWidth / 2, + this.plotOffset.top + this.plotHeight / 2); + + radius = this.plotHeight * options.radar.radiusRatio / 2 + options.fontSize; + sides = this.axes.x.ticks.length; + coeff = 2 * (Math.PI / sides); + angle = -Math.PI / 2; + + drawLabelCircular(this, a.x, false); + drawLabelCircular(this, a.x, true); + drawLabelCircular(this, a.y, false); + drawLabelCircular(this, a.y, true); + ctx.restore(); + } + + if (!options.HtmlText && this.textEnabled) { + drawLabelNoHtmlText(this, a.x, 'center', 'top'); + drawLabelNoHtmlText(this, a.x2, 'center', 'bottom'); + drawLabelNoHtmlText(this, a.y, 'right', 'middle'); + drawLabelNoHtmlText(this, a.y2, 'left', 'middle'); + + } else if (( + a.x.options.showLabels || + a.x2.options.showLabels || + a.y.options.showLabels || + a.y2.options.showLabels) && + !options.grid.circular + ) { + + html = ''; + + drawLabelHtml(this, a.x); + drawLabelHtml(this, a.x2); + drawLabelHtml(this, a.y); + drawLabelHtml(this, a.y2); + + ctx.stroke(); + ctx.restore(); + div = D.create('div'); + D.setStyles(div, { + fontSize: 'smaller', + color: options.grid.color + }); + div.className = 'flotr-labels'; + D.insert(this.el, div); + D.insert(div, html); + } + + function drawLabelCircular (graph, axis, minorTicks) { + var + ticks = minorTicks ? axis.minorTicks : axis.ticks, + isX = axis.orientation === 1, + isFirst = axis.n === 1, + style, offset; + + style = { + color : axis.options.color || options.grid.color, + angle : Flotr.toRad(axis.options.labelsAngle), + textBaseline : 'middle' + }; + + for (i = 0; i < ticks.length && + (minorTicks ? axis.options.showMinorLabels : axis.options.showLabels); ++i){ + tick = ticks[i]; + tick.label += ''; + if (!tick.label || !tick.label.length) { continue; } + + x = Math.cos(i * coeff + angle) * radius; + y = Math.sin(i * coeff + angle) * radius; + + style.textAlign = isX ? (Math.abs(x) < 0.1 ? 'center' : (x < 0 ? 'right' : 'left')) : 'left'; + + Flotr.drawText( + ctx, tick.label, + isX ? x : 3, + isX ? y : -(axis.ticks[i].v / axis.max) * (radius - options.fontSize), + style + ); + } + } + + function drawLabelNoHtmlText (graph, axis, textAlign, textBaseline) { + var + isX = axis.orientation === 1, + isFirst = axis.n === 1, + style, offset; + + style = { + color : axis.options.color || options.grid.color, + textAlign : textAlign, + textBaseline : textBaseline, + angle : Flotr.toRad(axis.options.labelsAngle) + }; + style = Flotr.getBestTextAlign(style.angle, style); + + for (i = 0; i < axis.ticks.length && continueShowingLabels(axis); ++i) { + + tick = axis.ticks[i]; + if (!tick.label || !tick.label.length) { continue; } + + offset = axis.d2p(tick.v); + if (offset < 0 || + offset > (isX ? graph.plotWidth : graph.plotHeight)) { continue; } + + Flotr.drawText( + ctx, tick.label, + leftOffset(graph, isX, isFirst, offset), + topOffset(graph, isX, isFirst, offset), + style + ); + + // Only draw on axis y2 + if (!isX && !isFirst) { + ctx.save(); + ctx.strokeStyle = style.color; + ctx.beginPath(); + ctx.moveTo(graph.plotOffset.left + graph.plotWidth - 8, graph.plotOffset.top + axis.d2p(tick.v)); + ctx.lineTo(graph.plotOffset.left + graph.plotWidth, graph.plotOffset.top + axis.d2p(tick.v)); + ctx.stroke(); + ctx.restore(); + } + } + + function continueShowingLabels (axis) { + return axis.options.showLabels && axis.used; + } + function leftOffset (graph, isX, isFirst, offset) { + return graph.plotOffset.left + + (isX ? offset : + (isFirst ? + -options.grid.labelMargin : + options.grid.labelMargin + graph.plotWidth)); + } + function topOffset (graph, isX, isFirst, offset) { + return graph.plotOffset.top + + (isX ? options.grid.labelMargin : offset) + + ((isX && isFirst) ? graph.plotHeight : 0); + } + } + + function drawLabelHtml (graph, axis) { + var + isX = axis.orientation === 1, + isFirst = axis.n === 1, + name = '', + left, style, top, + offset = graph.plotOffset; + + if (!isX && !isFirst) { + ctx.save(); + ctx.strokeStyle = axis.options.color || options.grid.color; + ctx.beginPath(); + } + + if (axis.options.showLabels && (isFirst ? true : axis.used)) { + for (i = 0; i < axis.ticks.length; ++i) { + tick = axis.ticks[i]; + if (!tick.label || !tick.label.length || + ((isX ? offset.left : offset.top) + axis.d2p(tick.v) < 0) || + ((isX ? offset.left : offset.top) + axis.d2p(tick.v) > (isX ? graph.canvasWidth : graph.canvasHeight))) { + continue; + } + top = offset.top + + (isX ? + ((isFirst ? 1 : -1 ) * (graph.plotHeight + options.grid.labelMargin)) : + axis.d2p(tick.v) - axis.maxLabel.height / 2); + left = isX ? (offset.left + axis.d2p(tick.v) - xBoxWidth / 2) : 0; + + name = ''; + if (i === 0) { + name = ' first'; + } else if (i === axis.ticks.length - 1) { + name = ' last'; + } + name += isX ? ' flotr-grid-label-x' : ' flotr-grid-label-y'; + + html += [ + '
' + tick.label + '
' + ].join(' '); + + if (!isX && !isFirst) { + ctx.moveTo(offset.left + graph.plotWidth - 8, offset.top + axis.d2p(tick.v)); + ctx.lineTo(offset.left + graph.plotWidth, offset.top + axis.d2p(tick.v)); + } + } + } + } + } + +}); +})(); + +(function () { + +var + D = Flotr.DOM, + _ = Flotr._; + +Flotr.addPlugin('legend', { + options: { + show: true, // => setting to true will show the legend, hide otherwise + noColumns: 1, // => number of colums in legend table // @todo: doesn't work for HtmlText = false + labelFormatter: function(v){return v;}, // => fn: string -> string + labelBoxBorderColor: '#CCCCCC', // => border color for the little label boxes + labelBoxWidth: 14, + labelBoxHeight: 10, + labelBoxMargin: 5, + container: null, // => container (as jQuery object) to put legend in, null means default on top of graph + position: 'nw', // => position of default legend container within plot + margin: 5, // => distance from grid edge to default legend container within plot + backgroundColor: '#F0F0F0', // => Legend background color. + backgroundOpacity: 0.85// => set to 0 to avoid background, set to 1 for a solid background + }, + callbacks: { + 'flotr:afterinit': function() { + this.legend.insertLegend(); + } + }, + /** + * Adds a legend div to the canvas container or draws it on the canvas. + */ + insertLegend: function(){ + + if(!this.options.legend.show) + return; + + var series = this.series, + plotOffset = this.plotOffset, + options = this.options, + legend = options.legend, + fragments = [], + rowStarted = false, + ctx = this.ctx, + itemCount = _.filter(series, function(s) {return (s.label && !s.hide);}).length, + p = legend.position, + m = legend.margin, + opacity = legend.backgroundOpacity, + i, label, color; + + if (itemCount) { + + var lbw = legend.labelBoxWidth, + lbh = legend.labelBoxHeight, + lbm = legend.labelBoxMargin, + offsetX = plotOffset.left + m, + offsetY = plotOffset.top + m, + labelMaxWidth = 0, + style = { + size: options.fontSize*1.1, + color: options.grid.color + }; + + // We calculate the labels' max width + for(i = series.length - 1; i > -1; --i){ + if(!series[i].label || series[i].hide) continue; + label = legend.labelFormatter(series[i].label); + labelMaxWidth = Math.max(labelMaxWidth, this._text.measureText(label, style).width); + } + + var legendWidth = Math.round(lbw + lbm*3 + labelMaxWidth), + legendHeight = Math.round(itemCount*(lbm+lbh) + lbm); + + // Default Opacity + if (!opacity && !opacity === 0) { + opacity = 0.1; + } + + if (!options.HtmlText && this.textEnabled && !legend.container) { + + if(p.charAt(0) == 's') offsetY = plotOffset.top + this.plotHeight - (m + legendHeight); + if(p.charAt(0) == 'c') offsetY = plotOffset.top + (this.plotHeight/2) - (m + (legendHeight/2)); + if(p.charAt(1) == 'e') offsetX = plotOffset.left + this.plotWidth - (m + legendWidth); + + // Legend box + color = this.processColor(legend.backgroundColor, { opacity : opacity }); + + ctx.fillStyle = color; + ctx.fillRect(offsetX, offsetY, legendWidth, legendHeight); + ctx.strokeStyle = legend.labelBoxBorderColor; + ctx.strokeRect(Flotr.toPixel(offsetX), Flotr.toPixel(offsetY), legendWidth, legendHeight); + + // Legend labels + var x = offsetX + lbm; + var y = offsetY + lbm; + for(i = 0; i < series.length; i++){ + if(!series[i].label || series[i].hide) continue; + label = legend.labelFormatter(series[i].label); + + ctx.fillStyle = series[i].color; + ctx.fillRect(x, y, lbw-1, lbh-1); + + ctx.strokeStyle = legend.labelBoxBorderColor; + ctx.lineWidth = 1; + ctx.strokeRect(Math.ceil(x)-1.5, Math.ceil(y)-1.5, lbw+2, lbh+2); + + // Legend text + Flotr.drawText(ctx, label, x + lbw + lbm, y + lbh, style); + + y += lbh + lbm; + } + } + else { + for(i = 0; i < series.length; ++i){ + if(!series[i].label || series[i].hide) continue; + + if(i % legend.noColumns === 0){ + fragments.push(rowStarted ? '
' : ''); + rowStarted = true; + } + + var s = series[i], + boxWidth = legend.labelBoxWidth, + boxHeight = legend.labelBoxHeight; + + label = legend.labelFormatter(s.label); + color = 'background-color:' + ((s.bars && s.bars.show && s.bars.fillColor && s.bars.fill) ? s.bars.fillColor : s.color) + ';'; + + fragments.push( + '', + '' + ); + } + if(rowStarted) fragments.push(''); + + if(fragments.length > 0){ + var table = '
 '+(serie.label || String.fromCharCode(65+i))+'
', + '
', + '
', // Border + '
', // Background + '
', + '
', + '
', label, '
' + fragments.join('') + '
'; + if(legend.container){ + D.empty(legend.container); + D.insert(legend.container, table); + } + else { + var styles = {position: 'absolute', 'zIndex': '2', 'border' : '1px solid ' + legend.labelBoxBorderColor}; + + if(p.charAt(0) == 'n') { styles.top = (m + plotOffset.top) + 'px'; styles.bottom = 'auto'; } + else if(p.charAt(0) == 'c') { styles.top = (m + (this.plotHeight - legendHeight) / 2) + 'px'; styles.bottom = 'auto'; } + else if(p.charAt(0) == 's') { styles.bottom = (m + plotOffset.bottom) + 'px'; styles.top = 'auto'; } + if(p.charAt(1) == 'e') { styles.right = (m + plotOffset.right) + 'px'; styles.left = 'auto'; } + else if(p.charAt(1) == 'w') { styles.left = (m + plotOffset.left) + 'px'; styles.right = 'auto'; } + + var div = D.create('div'), size; + div.className = 'flotr-legend'; + D.setStyles(div, styles); + D.insert(div, table); + D.insert(this.el, div); + + if (!opacity) return; + + var c = legend.backgroundColor || options.grid.backgroundColor || '#ffffff'; + + _.extend(styles, D.size(div), { + 'backgroundColor': c, + 'zIndex' : '', + 'border' : '' + }); + styles.width += 'px'; + styles.height += 'px'; + + // Put in the transparent background separately to avoid blended labels and + div = D.create('div'); + div.className = 'flotr-legend-bg'; + D.setStyles(div, styles); + D.opacity(div, opacity); + D.insert(div, ' '); + D.insert(this.el, div); + } + } + } + } + } +}); +})(); + +/** Spreadsheet **/ +(function() { + +function getRowLabel(value){ + if (this.options.spreadsheet.tickFormatter){ + //TODO maybe pass the xaxis formatter to the custom tick formatter as an opt-out? + return this.options.spreadsheet.tickFormatter(value); + } + else { + var t = _.find(this.axes.x.ticks, function(t){return t.v == value;}); + if (t) { + return t.label; + } + return value; + } +} + +var + D = Flotr.DOM, + _ = Flotr._; + +Flotr.addPlugin('spreadsheet', { + options: { + show: false, // => show the data grid using two tabs + tabGraphLabel: 'Graph', + tabDataLabel: 'Data', + toolbarDownload: 'Download CSV', // @todo: add better language support + toolbarSelectAll: 'Select all', + csvFileSeparator: ',', + decimalSeparator: '.', + tickFormatter: null, + initialTab: 'graph' + }, + /** + * Builds the tabs in the DOM + */ + callbacks: { + 'flotr:afterconstruct': function(){ + // @TODO necessary? + //this.el.select('.flotr-tabs-group,.flotr-datagrid-container').invoke('remove'); + + if (!this.options.spreadsheet.show) return; + + var ss = this.spreadsheet, + container = D.node('
'), + graph = D.node('
'+this.options.spreadsheet.tabGraphLabel+'
'), + data = D.node('
'+this.options.spreadsheet.tabDataLabel+'
'), + offset; + + ss.tabsContainer = container; + ss.tabs = { graph : graph, data : data }; + + D.insert(container, graph); + D.insert(container, data); + D.insert(this.el, container); + + offset = D.size(data).height + 2; + this.plotOffset.bottom += offset; + + D.setStyles(container, {top: this.canvasHeight-offset+'px'}); + + this. + observe(graph, 'click', function(){ss.showTab('graph');}). + observe(data, 'click', function(){ss.showTab('data');}); + if (this.options.spreadsheet.initialTab !== 'graph'){ + ss.showTab(this.options.spreadsheet.initialTab); + } + } + }, + /** + * Builds a matrix of the data to make the correspondance between the x values and the y values : + * X value => Y values from the axes + * @return {Array} The data grid + */ + loadDataGrid: function(){ + if (this.seriesData) return this.seriesData; + + var s = this.series, + rows = {}; + + /* The data grid is a 2 dimensions array. There is a row for each X value. + * Each row contains the x value and the corresponding y value for each serie ('undefined' if there isn't one) + **/ + _.each(s, function(serie, i){ + _.each(serie.data, function (v) { + var x = v[0], + y = v[1], + r = rows[x]; + if (r) { + r[i+1] = y; + } else { + var newRow = []; + newRow[0] = x; + newRow[i+1] = y; + rows[x] = newRow; + } + }); + }); + + // The data grid is sorted by x value + this.seriesData = _.sortBy(rows, function(row, x){ + return parseInt(x, 10); + }); + return this.seriesData; + }, + /** + * Constructs the data table for the spreadsheet + * @todo make a spreadsheet manager (Flotr.Spreadsheet) + * @return {Element} The resulting table element + */ + constructDataGrid: function(){ + // If the data grid has already been built, nothing to do here + if (this.spreadsheet.datagrid) return this.spreadsheet.datagrid; + + var s = this.series, + datagrid = this.spreadsheet.loadDataGrid(), + colgroup = [''], + buttonDownload, buttonSelect, t; + + // First row : series' labels + var html = ['']; + html.push(''); + _.each(s, function(serie,i){ + html.push(''); + colgroup.push(''); + }); + html.push(''); + // Data rows + _.each(datagrid, function(row){ + html.push(''); + _.times(s.length+1, function(i){ + var tag = 'td', + value = row[i], + // TODO: do we really want to handle problems with floating point + // precision here? + content = (!_.isUndefined(value) ? Math.round(value*100000)/100000 : ''); + if (i === 0) { + tag = 'th'; + var label = getRowLabel.call(this, content); + if (label) content = label; + } + + html.push('<'+tag+(tag=='th'?' scope="row"':'')+'>'+content+''); + }, this); + html.push(''); + }, this); + colgroup.push(''); + t = D.node(html.join('')); + + /** + * @TODO disabled this + if (!Flotr.isIE || Flotr.isIE == 9) { + function handleMouseout(){ + t.select('colgroup col.hover, th.hover').invoke('removeClassName', 'hover'); + } + function handleMouseover(e){ + var td = e.element(), + siblings = td.previousSiblings(); + t.select('th[scope=col]')[siblings.length-1].addClassName('hover'); + t.select('colgroup col')[siblings.length].addClassName('hover'); + } + _.each(t.select('td'), function(td) { + Flotr.EventAdapter. + observe(td, 'mouseover', handleMouseover). + observe(td, 'mouseout', handleMouseout); + }); + } + */ + + buttonDownload = D.node( + ''); + + buttonSelect = D.node( + ''); + + this. + observe(buttonDownload, 'click', _.bind(this.spreadsheet.downloadCSV, this)). + observe(buttonSelect, 'click', _.bind(this.spreadsheet.selectAllData, this)); + + var toolbar = D.node('
'); + D.insert(toolbar, buttonDownload); + D.insert(toolbar, buttonSelect); + + var containerHeight =this.canvasHeight - D.size(this.spreadsheet.tabsContainer).height-2, + container = D.node('
'); + + D.insert(container, toolbar); + D.insert(container, t); + D.insert(this.el, container); + this.spreadsheet.datagrid = t; + this.spreadsheet.container = container; + + return t; + }, + /** + * Shows the specified tab, by its name + * @todo make a tab manager (Flotr.Tabs) + * @param {String} tabName - The tab name + */ + showTab: function(tabName){ + if (this.spreadsheet.activeTab === tabName){ + return; + } + switch(tabName) { + case 'graph': + D.hide(this.spreadsheet.container); + D.removeClass(this.spreadsheet.tabs.data, 'selected'); + D.addClass(this.spreadsheet.tabs.graph, 'selected'); + break; + case 'data': + if (!this.spreadsheet.datagrid) + this.spreadsheet.constructDataGrid(); + D.show(this.spreadsheet.container); + D.addClass(this.spreadsheet.tabs.data, 'selected'); + D.removeClass(this.spreadsheet.tabs.graph, 'selected'); + break; + default: + throw 'Illegal tab name: ' + tabName; + } + this.spreadsheet.activeTab = tabName; + }, + /** + * Selects the data table in the DOM for copy/paste + */ + selectAllData: function(){ + if (this.spreadsheet.tabs) { + var selection, range, doc, win, node = this.spreadsheet.constructDataGrid(); + + this.spreadsheet.showTab('data'); + + // deferred to be able to select the table + setTimeout(function () { + if ((doc = node.ownerDocument) && (win = doc.defaultView) && + win.getSelection && doc.createRange && + (selection = window.getSelection()) && + selection.removeAllRanges) { + range = doc.createRange(); + range.selectNode(node); + selection.removeAllRanges(); + selection.addRange(range); + } + else if (document.body && document.body.createTextRange && + (range = document.body.createTextRange())) { + range.moveToElementText(node); + range.select(); + } + }, 0); + return true; + } + else return false; + }, + /** + * Converts the data into CSV in order to download a file + */ + downloadCSV: function(){ + var csv = '', + series = this.series, + options = this.options, + dg = this.spreadsheet.loadDataGrid(), + separator = encodeURIComponent(options.spreadsheet.csvFileSeparator); + + if (options.spreadsheet.decimalSeparator === options.spreadsheet.csvFileSeparator) { + throw "The decimal separator is the same as the column separator ("+options.spreadsheet.decimalSeparator+")"; + } + + // The first row + _.each(series, function(serie, i){ + csv += separator+'"'+(serie.label || String.fromCharCode(65+i)).replace(/\"/g, '\\"')+'"'; + }); + + csv += "%0D%0A"; // \r\n + + // For each row + csv += _.reduce(dg, function(memo, row){ + var rowLabel = getRowLabel.call(this, row[0]) || ''; + rowLabel = '"'+(rowLabel+'').replace(/\"/g, '\\"')+'"'; + var numbers = row.slice(1).join(separator); + if (options.spreadsheet.decimalSeparator !== '.') { + numbers = numbers.replace(/\./g, options.spreadsheet.decimalSeparator); + } + return memo + rowLabel+separator+numbers+"%0D%0A"; // \t and \r\n + }, '', this); + + if (Flotr.isIE && Flotr.isIE < 9) { + csv = csv.replace(new RegExp(separator, 'g'), decodeURIComponent(separator)).replace(/%0A/g, '\n').replace(/%0D/g, '\r'); + window.open().document.write(csv); + } + else window.open('data:text/csv,'+csv); + } +}); +})(); + +(function () { + +var D = Flotr.DOM; + +Flotr.addPlugin('titles', { + callbacks: { + 'flotr:afterdraw': function() { + this.titles.drawTitles(); + } + }, + /** + * Draws the title and the subtitle + */ + drawTitles : function () { + var html, + options = this.options, + margin = options.grid.labelMargin, + ctx = this.ctx, + a = this.axes; + + if (!options.HtmlText && this.textEnabled) { + var style = { + size: options.fontSize, + color: options.grid.color, + textAlign: 'center' + }; + + // Add subtitle + if (options.subtitle){ + Flotr.drawText( + ctx, options.subtitle, + this.plotOffset.left + this.plotWidth/2, + this.titleHeight + this.subtitleHeight - 2, + style + ); + } + + style.weight = 1.5; + style.size *= 1.5; + + // Add title + if (options.title){ + Flotr.drawText( + ctx, options.title, + this.plotOffset.left + this.plotWidth/2, + this.titleHeight - 2, + style + ); + } + + style.weight = 1.8; + style.size *= 0.8; + + // Add x axis title + if (a.x.options.title && a.x.used){ + style.textAlign = a.x.options.titleAlign || 'center'; + style.textBaseline = 'top'; + style.angle = Flotr.toRad(a.x.options.titleAngle); + style = Flotr.getBestTextAlign(style.angle, style); + Flotr.drawText( + ctx, a.x.options.title, + this.plotOffset.left + this.plotWidth/2, + this.plotOffset.top + a.x.maxLabel.height + this.plotHeight + 2 * margin, + style + ); + } + + // Add x2 axis title + if (a.x2.options.title && a.x2.used){ + style.textAlign = a.x2.options.titleAlign || 'center'; + style.textBaseline = 'bottom'; + style.angle = Flotr.toRad(a.x2.options.titleAngle); + style = Flotr.getBestTextAlign(style.angle, style); + Flotr.drawText( + ctx, a.x2.options.title, + this.plotOffset.left + this.plotWidth/2, + this.plotOffset.top - a.x2.maxLabel.height - 2 * margin, + style + ); + } + + // Add y axis title + if (a.y.options.title && a.y.used){ + style.textAlign = a.y.options.titleAlign || 'right'; + style.textBaseline = 'middle'; + style.angle = Flotr.toRad(a.y.options.titleAngle); + style = Flotr.getBestTextAlign(style.angle, style); + Flotr.drawText( + ctx, a.y.options.title, + this.plotOffset.left - a.y.maxLabel.width - 2 * margin, + this.plotOffset.top + this.plotHeight / 2, + style + ); + } + + // Add y2 axis title + if (a.y2.options.title && a.y2.used){ + style.textAlign = a.y2.options.titleAlign || 'left'; + style.textBaseline = 'middle'; + style.angle = Flotr.toRad(a.y2.options.titleAngle); + style = Flotr.getBestTextAlign(style.angle, style); + Flotr.drawText( + ctx, a.y2.options.title, + this.plotOffset.left + this.plotWidth + a.y2.maxLabel.width + 2 * margin, + this.plotOffset.top + this.plotHeight / 2, + style + ); + } + } + else { + html = []; + + // Add title + if (options.title) + html.push( + '
', options.title, '
' + ); + + // Add subtitle + if (options.subtitle) + html.push( + '
', options.subtitle, '
' + ); + + html.push(''); + + html.push('
'); + + // Add x axis title + if (a.x.options.title && a.x.used) + html.push( + '
', a.x.options.title, '
' + ); + + // Add x2 axis title + if (a.x2.options.title && a.x2.used) + html.push( + '
', a.x2.options.title, '
' + ); + + // Add y axis title + if (a.y.options.title && a.y.used) + html.push( + '
', a.y.options.title, '
' + ); + + // Add y2 axis title + if (a.y2.options.title && a.y2.used) + html.push( + '
', a.y2.options.title, '
' + ); + + html = html.join(''); + + var div = D.create('div'); + D.setStyles({ + color: options.grid.color + }); + div.className = 'flotr-titles'; + D.insert(this.el, div); + D.insert(div, html); + } + } +}); +})(); diff --git a/ckanext/reclineview/theme/public/vendor/leaflet/0.7.7/leaflet.css b/ckanext/reclineview/theme/public/vendor/leaflet/0.7.7/leaflet.css index c161c3134a..dea175f0f0 100644 --- a/ckanext/reclineview/theme/public/vendor/leaflet/0.7.7/leaflet.css +++ b/ckanext/reclineview/theme/public/vendor/leaflet/0.7.7/leaflet.css @@ -1,479 +1,479 @@ -/* required styles */ - -.leaflet-map-pane, -.leaflet-tile, -.leaflet-marker-icon, -.leaflet-marker-shadow, -.leaflet-tile-pane, -.leaflet-tile-container, -.leaflet-overlay-pane, -.leaflet-shadow-pane, -.leaflet-marker-pane, -.leaflet-popup-pane, -.leaflet-overlay-pane svg, -.leaflet-zoom-box, -.leaflet-image-layer, -.leaflet-layer { - position: absolute; - left: 0; - top: 0; - } -.leaflet-container { - overflow: hidden; - -ms-touch-action: none; - touch-action: none; - } -.leaflet-tile, -.leaflet-marker-icon, -.leaflet-marker-shadow { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - -webkit-user-drag: none; - } -.leaflet-marker-icon, -.leaflet-marker-shadow { - display: block; - } -/* map is broken in FF if you have max-width: 100% on tiles */ -.leaflet-container img { - max-width: none !important; - } -/* stupid Android 2 doesn't understand "max-width: none" properly */ -.leaflet-container img.leaflet-image-layer { - max-width: 15000px !important; - } -.leaflet-tile { - filter: inherit; - visibility: hidden; - } -.leaflet-tile-loaded { - visibility: inherit; - } -.leaflet-zoom-box { - width: 0; - height: 0; - } -/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ -.leaflet-overlay-pane svg { - -moz-user-select: none; - } - -.leaflet-tile-pane { z-index: 2; } -.leaflet-objects-pane { z-index: 3; } -.leaflet-overlay-pane { z-index: 4; } -.leaflet-shadow-pane { z-index: 5; } -.leaflet-marker-pane { z-index: 6; } -.leaflet-popup-pane { z-index: 7; } - -.leaflet-vml-shape { - width: 1px; - height: 1px; - } -.lvml { - behavior: url(#default#VML); - display: inline-block; - position: absolute; - } - - -/* control positioning */ - -.leaflet-control { - position: relative; - z-index: 7; - pointer-events: auto; - } -.leaflet-top, -.leaflet-bottom { - position: absolute; - z-index: 1000; - pointer-events: none; - } -.leaflet-top { - top: 0; - } -.leaflet-right { - right: 0; - } -.leaflet-bottom { - bottom: 0; - } -.leaflet-left { - left: 0; - } -.leaflet-control { - float: left; - clear: both; - } -.leaflet-right .leaflet-control { - float: right; - } -.leaflet-top .leaflet-control { - margin-top: 10px; - } -.leaflet-bottom .leaflet-control { - margin-bottom: 10px; - } -.leaflet-left .leaflet-control { - margin-left: 10px; - } -.leaflet-right .leaflet-control { - margin-right: 10px; - } - - -/* zoom and fade animations */ - -.leaflet-fade-anim .leaflet-tile, -.leaflet-fade-anim .leaflet-popup { - opacity: 0; - -webkit-transition: opacity 0.2s linear; - -moz-transition: opacity 0.2s linear; - -o-transition: opacity 0.2s linear; - transition: opacity 0.2s linear; - } -.leaflet-fade-anim .leaflet-tile-loaded, -.leaflet-fade-anim .leaflet-map-pane .leaflet-popup { - opacity: 1; - } - -.leaflet-zoom-anim .leaflet-zoom-animated { - -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); - -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1); - -o-transition: -o-transform 0.25s cubic-bezier(0,0,0.25,1); - transition: transform 0.25s cubic-bezier(0,0,0.25,1); - } -.leaflet-zoom-anim .leaflet-tile, -.leaflet-pan-anim .leaflet-tile, -.leaflet-touching .leaflet-zoom-animated { - -webkit-transition: none; - -moz-transition: none; - -o-transition: none; - transition: none; - } - -.leaflet-zoom-anim .leaflet-zoom-hide { - visibility: hidden; - } - - -/* cursors */ - -.leaflet-clickable { - cursor: pointer; - } -.leaflet-container { - cursor: -webkit-grab; - cursor: -moz-grab; - } -.leaflet-popup-pane, -.leaflet-control { - cursor: auto; - } -.leaflet-dragging .leaflet-container, -.leaflet-dragging .leaflet-clickable { - cursor: move; - cursor: -webkit-grabbing; - cursor: -moz-grabbing; - } - - -/* visual tweaks */ - -.leaflet-container { - background: #ddd; - outline: 0; - } -.leaflet-container a { - color: #0078A8; - } -.leaflet-container a.leaflet-active { - outline: 2px solid orange; - } -.leaflet-zoom-box { - border: 2px dotted #38f; - background: rgba(255,255,255,0.5); - } - - -/* general typography */ -.leaflet-container { - font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; - } - - -/* general toolbar styles */ - -.leaflet-bar { - box-shadow: 0 1px 5px rgba(0,0,0,0.65); - border-radius: 4px; - } -.leaflet-bar a, -.leaflet-bar a:hover { - background-color: #fff; - border-bottom: 1px solid #ccc; - width: 26px; - height: 26px; - line-height: 26px; - display: block; - text-align: center; - text-decoration: none; - color: black; - } -.leaflet-bar a, -.leaflet-control-layers-toggle { - background-position: 50% 50%; - background-repeat: no-repeat; - display: block; - } -.leaflet-bar a:hover { - background-color: #f4f4f4; - } -.leaflet-bar a:first-child { - border-top-left-radius: 4px; - border-top-right-radius: 4px; - } -.leaflet-bar a:last-child { - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; - border-bottom: none; - } -.leaflet-bar a.leaflet-disabled { - cursor: default; - background-color: #f4f4f4; - color: #bbb; - } - -.leaflet-touch .leaflet-bar a { - width: 30px; - height: 30px; - line-height: 30px; - } - - -/* zoom control */ - -.leaflet-control-zoom-in, -.leaflet-control-zoom-out { - font: bold 18px 'Lucida Console', Monaco, monospace; - text-indent: 1px; - } -.leaflet-control-zoom-out { - font-size: 20px; - } - -.leaflet-touch .leaflet-control-zoom-in { - font-size: 22px; - } -.leaflet-touch .leaflet-control-zoom-out { - font-size: 24px; - } - - -/* layers control */ - -.leaflet-control-layers { - box-shadow: 0 1px 5px rgba(0,0,0,0.4); - background: #fff; - border-radius: 5px; - } -.leaflet-control-layers-toggle { - background-image: url(images/layers.png); - width: 36px; - height: 36px; - } -.leaflet-retina .leaflet-control-layers-toggle { - background-image: url(images/layers-2x.png); - background-size: 26px 26px; - } -.leaflet-touch .leaflet-control-layers-toggle { - width: 44px; - height: 44px; - } -.leaflet-control-layers .leaflet-control-layers-list, -.leaflet-control-layers-expanded .leaflet-control-layers-toggle { - display: none; - } -.leaflet-control-layers-expanded .leaflet-control-layers-list { - display: block; - position: relative; - } -.leaflet-control-layers-expanded { - padding: 6px 10px 6px 6px; - color: #333; - background: #fff; - } -.leaflet-control-layers-selector { - margin-top: 2px; - position: relative; - top: 1px; - } -.leaflet-control-layers label { - display: block; - } -.leaflet-control-layers-separator { - height: 0; - border-top: 1px solid #ddd; - margin: 5px -10px 5px -6px; - } - - -/* attribution and scale controls */ - -.leaflet-container .leaflet-control-attribution { - background: #fff; - background: rgba(255, 255, 255, 0.7); - margin: 0; - } -.leaflet-control-attribution, -.leaflet-control-scale-line { - padding: 0 5px; - color: #333; - } -.leaflet-control-attribution a { - text-decoration: none; - } -.leaflet-control-attribution a:hover { - text-decoration: underline; - } -.leaflet-container .leaflet-control-attribution, -.leaflet-container .leaflet-control-scale { - font-size: 11px; - } -.leaflet-left .leaflet-control-scale { - margin-left: 5px; - } -.leaflet-bottom .leaflet-control-scale { - margin-bottom: 5px; - } -.leaflet-control-scale-line { - border: 2px solid #777; - border-top: none; - line-height: 1.1; - padding: 2px 5px 1px; - font-size: 11px; - white-space: nowrap; - overflow: hidden; - -moz-box-sizing: content-box; - box-sizing: content-box; - - background: #fff; - background: rgba(255, 255, 255, 0.5); - } -.leaflet-control-scale-line:not(:first-child) { - border-top: 2px solid #777; - border-bottom: none; - margin-top: -2px; - } -.leaflet-control-scale-line:not(:first-child):not(:last-child) { - border-bottom: 2px solid #777; - } - -.leaflet-touch .leaflet-control-attribution, -.leaflet-touch .leaflet-control-layers, -.leaflet-touch .leaflet-bar { - box-shadow: none; - } -.leaflet-touch .leaflet-control-layers, -.leaflet-touch .leaflet-bar { - border: 2px solid rgba(0,0,0,0.2); - background-clip: padding-box; - } - - -/* popup */ - -.leaflet-popup { - position: absolute; - text-align: center; - } -.leaflet-popup-content-wrapper { - padding: 1px; - text-align: left; - border-radius: 12px; - } -.leaflet-popup-content { - margin: 13px 19px; - line-height: 1.4; - } -.leaflet-popup-content p { - margin: 18px 0; - } -.leaflet-popup-tip-container { - margin: 0 auto; - width: 40px; - height: 20px; - position: relative; - overflow: hidden; - } -.leaflet-popup-tip { - width: 17px; - height: 17px; - padding: 1px; - - margin: -10px auto 0; - - -webkit-transform: rotate(45deg); - -moz-transform: rotate(45deg); - -ms-transform: rotate(45deg); - -o-transform: rotate(45deg); - transform: rotate(45deg); - } -.leaflet-popup-content-wrapper, -.leaflet-popup-tip { - background: white; - - box-shadow: 0 3px 14px rgba(0,0,0,0.4); - } -.leaflet-container a.leaflet-popup-close-button { - position: absolute; - top: 0; - right: 0; - padding: 4px 4px 0 0; - text-align: center; - width: 18px; - height: 14px; - font: 16px/14px Tahoma, Verdana, sans-serif; - color: #c3c3c3; - text-decoration: none; - font-weight: bold; - background: transparent; - } -.leaflet-container a.leaflet-popup-close-button:hover { - color: #999; - } -.leaflet-popup-scrolled { - overflow: auto; - border-bottom: 1px solid #ddd; - border-top: 1px solid #ddd; - } - -.leaflet-oldie .leaflet-popup-content-wrapper { - zoom: 1; - } -.leaflet-oldie .leaflet-popup-tip { - width: 24px; - margin: 0 auto; - - -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; - filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); - } -.leaflet-oldie .leaflet-popup-tip-container { - margin-top: -1px; - } - -.leaflet-oldie .leaflet-control-zoom, -.leaflet-oldie .leaflet-control-layers, -.leaflet-oldie .leaflet-popup-content-wrapper, -.leaflet-oldie .leaflet-popup-tip { - border: 1px solid #999; - } - - -/* div icon */ - -.leaflet-div-icon { - background: #fff; - border: 1px solid #666; - } +/* required styles */ + +.leaflet-map-pane, +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-tile-pane, +.leaflet-tile-container, +.leaflet-overlay-pane, +.leaflet-shadow-pane, +.leaflet-marker-pane, +.leaflet-popup-pane, +.leaflet-overlay-pane svg, +.leaflet-zoom-box, +.leaflet-image-layer, +.leaflet-layer { + position: absolute; + left: 0; + top: 0; + } +.leaflet-container { + overflow: hidden; + -ms-touch-action: none; + touch-action: none; + } +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + -webkit-user-drag: none; + } +.leaflet-marker-icon, +.leaflet-marker-shadow { + display: block; + } +/* map is broken in FF if you have max-width: 100% on tiles */ +.leaflet-container img { + max-width: none !important; + } +/* stupid Android 2 doesn't understand "max-width: none" properly */ +.leaflet-container img.leaflet-image-layer { + max-width: 15000px !important; + } +.leaflet-tile { + filter: inherit; + visibility: hidden; + } +.leaflet-tile-loaded { + visibility: inherit; + } +.leaflet-zoom-box { + width: 0; + height: 0; + } +/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ +.leaflet-overlay-pane svg { + -moz-user-select: none; + } + +.leaflet-tile-pane { z-index: 2; } +.leaflet-objects-pane { z-index: 3; } +.leaflet-overlay-pane { z-index: 4; } +.leaflet-shadow-pane { z-index: 5; } +.leaflet-marker-pane { z-index: 6; } +.leaflet-popup-pane { z-index: 7; } + +.leaflet-vml-shape { + width: 1px; + height: 1px; + } +.lvml { + behavior: url(#default#VML); + display: inline-block; + position: absolute; + } + + +/* control positioning */ + +.leaflet-control { + position: relative; + z-index: 7; + pointer-events: auto; + } +.leaflet-top, +.leaflet-bottom { + position: absolute; + z-index: 1000; + pointer-events: none; + } +.leaflet-top { + top: 0; + } +.leaflet-right { + right: 0; + } +.leaflet-bottom { + bottom: 0; + } +.leaflet-left { + left: 0; + } +.leaflet-control { + float: left; + clear: both; + } +.leaflet-right .leaflet-control { + float: right; + } +.leaflet-top .leaflet-control { + margin-top: 10px; + } +.leaflet-bottom .leaflet-control { + margin-bottom: 10px; + } +.leaflet-left .leaflet-control { + margin-left: 10px; + } +.leaflet-right .leaflet-control { + margin-right: 10px; + } + + +/* zoom and fade animations */ + +.leaflet-fade-anim .leaflet-tile, +.leaflet-fade-anim .leaflet-popup { + opacity: 0; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + -o-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; + } +.leaflet-fade-anim .leaflet-tile-loaded, +.leaflet-fade-anim .leaflet-map-pane .leaflet-popup { + opacity: 1; + } + +.leaflet-zoom-anim .leaflet-zoom-animated { + -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); + -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1); + -o-transition: -o-transform 0.25s cubic-bezier(0,0,0.25,1); + transition: transform 0.25s cubic-bezier(0,0,0.25,1); + } +.leaflet-zoom-anim .leaflet-tile, +.leaflet-pan-anim .leaflet-tile, +.leaflet-touching .leaflet-zoom-animated { + -webkit-transition: none; + -moz-transition: none; + -o-transition: none; + transition: none; + } + +.leaflet-zoom-anim .leaflet-zoom-hide { + visibility: hidden; + } + + +/* cursors */ + +.leaflet-clickable { + cursor: pointer; + } +.leaflet-container { + cursor: -webkit-grab; + cursor: -moz-grab; + } +.leaflet-popup-pane, +.leaflet-control { + cursor: auto; + } +.leaflet-dragging .leaflet-container, +.leaflet-dragging .leaflet-clickable { + cursor: move; + cursor: -webkit-grabbing; + cursor: -moz-grabbing; + } + + +/* visual tweaks */ + +.leaflet-container { + background: #ddd; + outline: 0; + } +.leaflet-container a { + color: #0078A8; + } +.leaflet-container a.leaflet-active { + outline: 2px solid orange; + } +.leaflet-zoom-box { + border: 2px dotted #38f; + background: rgba(255,255,255,0.5); + } + + +/* general typography */ +.leaflet-container { + font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; + } + + +/* general toolbar styles */ + +.leaflet-bar { + box-shadow: 0 1px 5px rgba(0,0,0,0.65); + border-radius: 4px; + } +.leaflet-bar a, +.leaflet-bar a:hover { + background-color: #fff; + border-bottom: 1px solid #ccc; + width: 26px; + height: 26px; + line-height: 26px; + display: block; + text-align: center; + text-decoration: none; + color: black; + } +.leaflet-bar a, +.leaflet-control-layers-toggle { + background-position: 50% 50%; + background-repeat: no-repeat; + display: block; + } +.leaflet-bar a:hover { + background-color: #f4f4f4; + } +.leaflet-bar a:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + } +.leaflet-bar a:last-child { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom: none; + } +.leaflet-bar a.leaflet-disabled { + cursor: default; + background-color: #f4f4f4; + color: #bbb; + } + +.leaflet-touch .leaflet-bar a { + width: 30px; + height: 30px; + line-height: 30px; + } + + +/* zoom control */ + +.leaflet-control-zoom-in, +.leaflet-control-zoom-out { + font: bold 18px 'Lucida Console', Monaco, monospace; + text-indent: 1px; + } +.leaflet-control-zoom-out { + font-size: 20px; + } + +.leaflet-touch .leaflet-control-zoom-in { + font-size: 22px; + } +.leaflet-touch .leaflet-control-zoom-out { + font-size: 24px; + } + + +/* layers control */ + +.leaflet-control-layers { + box-shadow: 0 1px 5px rgba(0,0,0,0.4); + background: #fff; + border-radius: 5px; + } +.leaflet-control-layers-toggle { + background-image: url(images/layers.png); + width: 36px; + height: 36px; + } +.leaflet-retina .leaflet-control-layers-toggle { + background-image: url(images/layers-2x.png); + background-size: 26px 26px; + } +.leaflet-touch .leaflet-control-layers-toggle { + width: 44px; + height: 44px; + } +.leaflet-control-layers .leaflet-control-layers-list, +.leaflet-control-layers-expanded .leaflet-control-layers-toggle { + display: none; + } +.leaflet-control-layers-expanded .leaflet-control-layers-list { + display: block; + position: relative; + } +.leaflet-control-layers-expanded { + padding: 6px 10px 6px 6px; + color: #333; + background: #fff; + } +.leaflet-control-layers-selector { + margin-top: 2px; + position: relative; + top: 1px; + } +.leaflet-control-layers label { + display: block; + } +.leaflet-control-layers-separator { + height: 0; + border-top: 1px solid #ddd; + margin: 5px -10px 5px -6px; + } + + +/* attribution and scale controls */ + +.leaflet-container .leaflet-control-attribution { + background: #fff; + background: rgba(255, 255, 255, 0.7); + margin: 0; + } +.leaflet-control-attribution, +.leaflet-control-scale-line { + padding: 0 5px; + color: #333; + } +.leaflet-control-attribution a { + text-decoration: none; + } +.leaflet-control-attribution a:hover { + text-decoration: underline; + } +.leaflet-container .leaflet-control-attribution, +.leaflet-container .leaflet-control-scale { + font-size: 11px; + } +.leaflet-left .leaflet-control-scale { + margin-left: 5px; + } +.leaflet-bottom .leaflet-control-scale { + margin-bottom: 5px; + } +.leaflet-control-scale-line { + border: 2px solid #777; + border-top: none; + line-height: 1.1; + padding: 2px 5px 1px; + font-size: 11px; + white-space: nowrap; + overflow: hidden; + -moz-box-sizing: content-box; + box-sizing: content-box; + + background: #fff; + background: rgba(255, 255, 255, 0.5); + } +.leaflet-control-scale-line:not(:first-child) { + border-top: 2px solid #777; + border-bottom: none; + margin-top: -2px; + } +.leaflet-control-scale-line:not(:first-child):not(:last-child) { + border-bottom: 2px solid #777; + } + +.leaflet-touch .leaflet-control-attribution, +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + box-shadow: none; + } +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + border: 2px solid rgba(0,0,0,0.2); + background-clip: padding-box; + } + + +/* popup */ + +.leaflet-popup { + position: absolute; + text-align: center; + } +.leaflet-popup-content-wrapper { + padding: 1px; + text-align: left; + border-radius: 12px; + } +.leaflet-popup-content { + margin: 13px 19px; + line-height: 1.4; + } +.leaflet-popup-content p { + margin: 18px 0; + } +.leaflet-popup-tip-container { + margin: 0 auto; + width: 40px; + height: 20px; + position: relative; + overflow: hidden; + } +.leaflet-popup-tip { + width: 17px; + height: 17px; + padding: 1px; + + margin: -10px auto 0; + + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -ms-transform: rotate(45deg); + -o-transform: rotate(45deg); + transform: rotate(45deg); + } +.leaflet-popup-content-wrapper, +.leaflet-popup-tip { + background: white; + + box-shadow: 0 3px 14px rgba(0,0,0,0.4); + } +.leaflet-container a.leaflet-popup-close-button { + position: absolute; + top: 0; + right: 0; + padding: 4px 4px 0 0; + text-align: center; + width: 18px; + height: 14px; + font: 16px/14px Tahoma, Verdana, sans-serif; + color: #c3c3c3; + text-decoration: none; + font-weight: bold; + background: transparent; + } +.leaflet-container a.leaflet-popup-close-button:hover { + color: #999; + } +.leaflet-popup-scrolled { + overflow: auto; + border-bottom: 1px solid #ddd; + border-top: 1px solid #ddd; + } + +.leaflet-oldie .leaflet-popup-content-wrapper { + zoom: 1; + } +.leaflet-oldie .leaflet-popup-tip { + width: 24px; + margin: 0 auto; + + -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; + filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); + } +.leaflet-oldie .leaflet-popup-tip-container { + margin-top: -1px; + } + +.leaflet-oldie .leaflet-control-zoom, +.leaflet-oldie .leaflet-control-layers, +.leaflet-oldie .leaflet-popup-content-wrapper, +.leaflet-oldie .leaflet-popup-tip { + border: 1px solid #999; + } + + +/* div icon */ + +.leaflet-div-icon { + background: #fff; + border: 1px solid #666; + } diff --git a/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/slick.grid.js b/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/slick.grid.js index c12bae9bba..2cf8d94109 100644 --- a/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/slick.grid.js +++ b/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/slick.grid.js @@ -1,3422 +1,3422 @@ -/** - * @license - * (c) 2009-2013 Michael Leibman - * michael{dot}leibman{at}gmail{dot}com - * http://github.com/mleibman/slickgrid - * - * Distributed under MIT license. - * All rights reserved. - * - * SlickGrid v2.2 - * - * NOTES: - * Cell/row DOM manipulations are done directly bypassing jQuery's DOM manipulation methods. - * This increases the speed dramatically, but can only be done safely because there are no event handlers - * or data associated with any cell/row DOM nodes. Cell editors must make sure they implement .destroy() - * and do proper cleanup. - */ - -// make sure required JavaScript modules are loaded -if (typeof jQuery === "undefined") { - throw "SlickGrid requires jquery module to be loaded"; -} -if (!jQuery.fn.drag) { - throw "SlickGrid requires jquery.event.drag module to be loaded"; -} -if (typeof Slick === "undefined") { - throw "slick.core.js not loaded"; -} - - -(function ($) { - // Slick.Grid - $.extend(true, window, { - Slick: { - Grid: SlickGrid - } - }); - - // shared across all grids on the page - var scrollbarDimensions; - var maxSupportedCssHeight; // browser's breaking point - - ////////////////////////////////////////////////////////////////////////////////////////////// - // SlickGrid class implementation (available as Slick.Grid) - - /** - * Creates a new instance of the grid. - * @class SlickGrid - * @constructor - * @param {Node} container Container node to create the grid in. - * @param {Array,Object} data An array of objects for databinding. - * @param {Array} columns An array of column definitions. - * @param {Object} options Grid options. - **/ - function SlickGrid(container, data, columns, options) { - // settings - var defaults = { - explicitInitialization: false, - rowHeight: 25, - defaultColumnWidth: 80, - enableAddRow: false, - leaveSpaceForNewRows: false, - editable: false, - autoEdit: true, - enableCellNavigation: true, - enableColumnReorder: true, - asyncEditorLoading: false, - asyncEditorLoadDelay: 100, - forceFitColumns: false, - enableAsyncPostRender: false, - asyncPostRenderDelay: 50, - autoHeight: false, - editorLock: Slick.GlobalEditorLock, - showHeaderRow: false, - headerRowHeight: 25, - showTopPanel: false, - topPanelHeight: 25, - formatterFactory: null, - editorFactory: null, - cellFlashingCssClass: "flashing", - selectedCellCssClass: "selected", - multiSelect: true, - enableTextSelectionOnCells: false, - dataItemColumnValueExtractor: null, - fullWidthRows: false, - multiColumnSort: false, - defaultFormatter: defaultFormatter, - forceSyncScrolling: false, - addNewRowCssClass: "new-row" - }; - - var columnDefaults = { - name: "", - resizable: true, - sortable: false, - minWidth: 30, - rerenderOnResize: false, - headerCssClass: null, - defaultSortAsc: true, - focusable: true, - selectable: true - }; - - // scroller - var th; // virtual height - var h; // real scrollable height - var ph; // page height - var n; // number of pages - var cj; // "jumpiness" coefficient - - var page = 0; // current page - var offset = 0; // current page offset - var vScrollDir = 1; - - // private - var initialized = false; - var $container; - var uid = "slickgrid_" + Math.round(1000000 * Math.random()); - var self = this; - var $focusSink, $focusSink2; - var $headerScroller; - var $headers; - var $headerRow, $headerRowScroller, $headerRowSpacer; - var $topPanelScroller; - var $topPanel; - var $viewport; - var $canvas; - var $style; - var $boundAncestors; - var stylesheet, columnCssRulesL, columnCssRulesR; - var viewportH, viewportW; - var canvasWidth; - var viewportHasHScroll, viewportHasVScroll; - var headerColumnWidthDiff = 0, headerColumnHeightDiff = 0, // border+padding - cellWidthDiff = 0, cellHeightDiff = 0; - var absoluteColumnMinWidth; - - var tabbingDirection = 1; - var activePosX; - var activeRow, activeCell; - var activeCellNode = null; - var currentEditor = null; - var serializedEditorValue; - var editController; - - var rowsCache = {}; - var renderedRows = 0; - var numVisibleRows; - var prevScrollTop = 0; - var scrollTop = 0; - var lastRenderedScrollTop = 0; - var lastRenderedScrollLeft = 0; - var prevScrollLeft = 0; - var scrollLeft = 0; - - var selectionModel; - var selectedRows = []; - - var plugins = []; - var cellCssClasses = {}; - - var columnsById = {}; - var sortColumns = []; - var columnPosLeft = []; - var columnPosRight = []; - - - // async call handles - var h_editorLoader = null; - var h_render = null; - var h_postrender = null; - var postProcessedRows = {}; - var postProcessToRow = null; - var postProcessFromRow = null; - - // perf counters - var counter_rows_rendered = 0; - var counter_rows_removed = 0; - - // These two variables work around a bug with inertial scrolling in Webkit/Blink on Mac. - // See http://crbug.com/312427. - var rowNodeFromLastMouseWheelEvent; // this node must not be deleted while inertial scrolling - var zombieRowNodeFromLastMouseWheelEvent; // node that was hidden instead of getting deleted - - - ////////////////////////////////////////////////////////////////////////////////////////////// - // Initialization - - function init() { - $container = $(container); - if ($container.length < 1) { - throw new Error("SlickGrid requires a valid container, " + container + " does not exist in the DOM."); - } - - // calculate these only once and share between grid instances - maxSupportedCssHeight = maxSupportedCssHeight || getMaxSupportedCssHeight(); - scrollbarDimensions = scrollbarDimensions || measureScrollbar(); - - options = $.extend({}, defaults, options); - validateAndEnforceOptions(); - columnDefaults.width = options.defaultColumnWidth; - - columnsById = {}; - for (var i = 0; i < columns.length; i++) { - var m = columns[i] = $.extend({}, columnDefaults, columns[i]); - columnsById[m.id] = i; - if (m.minWidth && m.width < m.minWidth) { - m.width = m.minWidth; - } - if (m.maxWidth && m.width > m.maxWidth) { - m.width = m.maxWidth; - } - } - - // validate loaded JavaScript modules against requested options - if (options.enableColumnReorder && !$.fn.sortable) { - throw new Error("SlickGrid's 'enableColumnReorder = true' option requires jquery-ui.sortable module to be loaded"); - } - - editController = { - "commitCurrentEdit": commitCurrentEdit, - "cancelCurrentEdit": cancelCurrentEdit - }; - - $container - .empty() - .css("overflow", "hidden") - .css("outline", 0) - .addClass(uid) - .addClass("ui-widget"); - - // set up a positioning container if needed - if (!/relative|absolute|fixed/.test($container.css("position"))) { - $container.css("position", "relative"); - } - - $focusSink = $("
").appendTo($container); - - $headerScroller = $("
").appendTo($container); - $headers = $("
").appendTo($headerScroller); - $headers.width(getHeadersWidth()); - - $headerRowScroller = $("
").appendTo($container); - $headerRow = $("
").appendTo($headerRowScroller); - $headerRowSpacer = $("
") - .css("width", getCanvasWidth() + scrollbarDimensions.width + "px") - .appendTo($headerRowScroller); - - $topPanelScroller = $("
").appendTo($container); - $topPanel = $("
").appendTo($topPanelScroller); - - if (!options.showTopPanel) { - $topPanelScroller.hide(); - } - - if (!options.showHeaderRow) { - $headerRowScroller.hide(); - } - - $viewport = $("
").appendTo($container); - $viewport.css("overflow-y", options.autoHeight ? "hidden" : "auto"); - - $canvas = $("
").appendTo($viewport); - - $focusSink2 = $focusSink.clone().appendTo($container); - - if (!options.explicitInitialization) { - finishInitialization(); - } - } - - function finishInitialization() { - if (!initialized) { - initialized = true; - - viewportW = parseFloat($.css($container[0], "width", true)); - - // header columns and cells may have different padding/border skewing width calculations (box-sizing, hello?) - // calculate the diff so we can set consistent sizes - measureCellPaddingAndBorder(); - - // for usability reasons, all text selection in SlickGrid is disabled - // with the exception of input and textarea elements (selection must - // be enabled there so that editors work as expected); note that - // selection in grid cells (grid body) is already unavailable in - // all browsers except IE - disableSelection($headers); // disable all text selection in header (including input and textarea) - - if (!options.enableTextSelectionOnCells) { - // disable text selection in grid cells except in input and textarea elements - // (this is IE-specific, because selectstart event will only fire in IE) - $viewport.bind("selectstart.ui", function (event) { - return $(event.target).is("input,textarea"); - }); - } - - updateColumnCaches(); - createColumnHeaders(); - setupColumnSort(); - createCssRules(); - resizeCanvas(); - bindAncestorScrollEvents(); - - $container - .bind("resize.slickgrid", resizeCanvas); - $viewport - //.bind("click", handleClick) - .bind("scroll", handleScroll); - $headerScroller - .bind("contextmenu", handleHeaderContextMenu) - .bind("click", handleHeaderClick) - .delegate(".slick-header-column", "mouseenter", handleHeaderMouseEnter) - .delegate(".slick-header-column", "mouseleave", handleHeaderMouseLeave); - $headerRowScroller - .bind("scroll", handleHeaderRowScroll); - $focusSink.add($focusSink2) - .bind("keydown", handleKeyDown); - $canvas - .bind("keydown", handleKeyDown) - .bind("click", handleClick) - .bind("dblclick", handleDblClick) - .bind("contextmenu", handleContextMenu) - .bind("draginit", handleDragInit) - .bind("dragstart", {distance: 3}, handleDragStart) - .bind("drag", handleDrag) - .bind("dragend", handleDragEnd) - .delegate(".slick-cell", "mouseenter", handleMouseEnter) - .delegate(".slick-cell", "mouseleave", handleMouseLeave); - - // Work around http://crbug.com/312427. - if (navigator.userAgent.toLowerCase().match(/webkit/) && - navigator.userAgent.toLowerCase().match(/macintosh/)) { - $canvas.bind("mousewheel", handleMouseWheel); - } - } - } - - function registerPlugin(plugin) { - plugins.unshift(plugin); - plugin.init(self); - } - - function unregisterPlugin(plugin) { - for (var i = plugins.length; i >= 0; i--) { - if (plugins[i] === plugin) { - if (plugins[i].destroy) { - plugins[i].destroy(); - } - plugins.splice(i, 1); - break; - } - } - } - - function setSelectionModel(model) { - if (selectionModel) { - selectionModel.onSelectedRangesChanged.unsubscribe(handleSelectedRangesChanged); - if (selectionModel.destroy) { - selectionModel.destroy(); - } - } - - selectionModel = model; - if (selectionModel) { - selectionModel.init(self); - selectionModel.onSelectedRangesChanged.subscribe(handleSelectedRangesChanged); - } - } - - function getSelectionModel() { - return selectionModel; - } - - function getCanvasNode() { - return $canvas[0]; - } - - function measureScrollbar() { - var $c = $("
").appendTo("body"); - var dim = { - width: $c.width() - $c[0].clientWidth, - height: $c.height() - $c[0].clientHeight - }; - $c.remove(); - return dim; - } - - function getHeadersWidth() { - var headersWidth = 0; - for (var i = 0, ii = columns.length; i < ii; i++) { - var width = columns[i].width; - headersWidth += width; - } - headersWidth += scrollbarDimensions.width; - return Math.max(headersWidth, viewportW) + 1000; - } - - function getCanvasWidth() { - var availableWidth = viewportHasVScroll ? viewportW - scrollbarDimensions.width : viewportW; - var rowWidth = 0; - var i = columns.length; - while (i--) { - rowWidth += columns[i].width; - } - return options.fullWidthRows ? Math.max(rowWidth, availableWidth) : rowWidth; - } - - function updateCanvasWidth(forceColumnWidthsUpdate) { - var oldCanvasWidth = canvasWidth; - canvasWidth = getCanvasWidth(); - - if (canvasWidth != oldCanvasWidth) { - $canvas.width(canvasWidth); - $headerRow.width(canvasWidth); - $headers.width(getHeadersWidth()); - viewportHasHScroll = (canvasWidth > viewportW - scrollbarDimensions.width); - } - - $headerRowSpacer.width(canvasWidth + (viewportHasVScroll ? scrollbarDimensions.width : 0)); - - if (canvasWidth != oldCanvasWidth || forceColumnWidthsUpdate) { - applyColumnWidths(); - } - } - - function disableSelection($target) { - if ($target && $target.jquery) { - $target - .attr("unselectable", "on") - .css("MozUserSelect", "none") - .bind("selectstart.ui", function () { - return false; - }); // from jquery:ui.core.js 1.7.2 - } - } - - function getMaxSupportedCssHeight() { - var supportedHeight = 1000000; - // FF reports the height back but still renders blank after ~6M px - var testUpTo = navigator.userAgent.toLowerCase().match(/firefox/) ? 6000000 : 1000000000; - var div = $("
").appendTo(document.body); - - while (true) { - var test = supportedHeight * 2; - div.css("height", test); - if (test > testUpTo || div.height() !== test) { - break; - } else { - supportedHeight = test; - } - } - - div.remove(); - return supportedHeight; - } - - // TODO: this is static. need to handle page mutation. - function bindAncestorScrollEvents() { - var elem = $canvas[0]; - while ((elem = elem.parentNode) != document.body && elem != null) { - // bind to scroll containers only - if (elem == $viewport[0] || elem.scrollWidth != elem.clientWidth || elem.scrollHeight != elem.clientHeight) { - var $elem = $(elem); - if (!$boundAncestors) { - $boundAncestors = $elem; - } else { - $boundAncestors = $boundAncestors.add($elem); - } - $elem.bind("scroll." + uid, handleActiveCellPositionChange); - } - } - } - - function unbindAncestorScrollEvents() { - if (!$boundAncestors) { - return; - } - $boundAncestors.unbind("scroll." + uid); - $boundAncestors = null; - } - - function updateColumnHeader(columnId, title, toolTip) { - if (!initialized) { return; } - var idx = getColumnIndex(columnId); - if (idx == null) { - return; - } - - var columnDef = columns[idx]; - var $header = $headers.children().eq(idx); - if ($header) { - if (title !== undefined) { - columns[idx].name = title; - } - if (toolTip !== undefined) { - columns[idx].toolTip = toolTip; - } - - trigger(self.onBeforeHeaderCellDestroy, { - "node": $header[0], - "column": columnDef - }); - - $header - .attr("title", toolTip || "") - .children().eq(0).html(title); - - trigger(self.onHeaderCellRendered, { - "node": $header[0], - "column": columnDef - }); - } - } - - function getHeaderRow() { - return $headerRow[0]; - } - - function getHeaderRowColumn(columnId) { - var idx = getColumnIndex(columnId); - var $header = $headerRow.children().eq(idx); - return $header && $header[0]; - } - - function createColumnHeaders() { - function onMouseEnter() { - $(this).addClass("ui-state-hover"); - } - - function onMouseLeave() { - $(this).removeClass("ui-state-hover"); - } - - $headers.find(".slick-header-column") - .each(function() { - var columnDef = $(this).data("column"); - if (columnDef) { - trigger(self.onBeforeHeaderCellDestroy, { - "node": this, - "column": columnDef - }); - } - }); - $headers.empty(); - $headers.width(getHeadersWidth()); - - $headerRow.find(".slick-headerrow-column") - .each(function() { - var columnDef = $(this).data("column"); - if (columnDef) { - trigger(self.onBeforeHeaderRowCellDestroy, { - "node": this, - "column": columnDef - }); - } - }); - $headerRow.empty(); - - for (var i = 0; i < columns.length; i++) { - var m = columns[i]; - - var header = $("
") - .html("" + m.name + "") - .width(m.width - headerColumnWidthDiff) - .attr("id", "" + uid + m.id) - .attr("title", m.toolTip || "") - .data("column", m) - .addClass(m.headerCssClass || "") - .appendTo($headers); - - if (options.enableColumnReorder || m.sortable) { - header - .on('mouseenter', onMouseEnter) - .on('mouseleave', onMouseLeave); - } - - if (m.sortable) { - header.addClass("slick-header-sortable"); - header.append(""); - } - - trigger(self.onHeaderCellRendered, { - "node": header[0], - "column": m - }); - - if (options.showHeaderRow) { - var headerRowCell = $("
") - .data("column", m) - .appendTo($headerRow); - - trigger(self.onHeaderRowCellRendered, { - "node": headerRowCell[0], - "column": m - }); - } - } - - setSortColumns(sortColumns); - setupColumnResize(); - if (options.enableColumnReorder) { - setupColumnReorder(); - } - } - - function setupColumnSort() { - $headers.click(function (e) { - // temporary workaround for a bug in jQuery 1.7.1 (http://bugs.jquery.com/ticket/11328) - e.metaKey = e.metaKey || e.ctrlKey; - - if ($(e.target).hasClass("slick-resizable-handle")) { - return; - } - - var $col = $(e.target).closest(".slick-header-column"); - if (!$col.length) { - return; - } - - var column = $col.data("column"); - if (column.sortable) { - if (!getEditorLock().commitCurrentEdit()) { - return; - } - - var sortOpts = null; - var i = 0; - for (; i < sortColumns.length; i++) { - if (sortColumns[i].columnId == column.id) { - sortOpts = sortColumns[i]; - sortOpts.sortAsc = !sortOpts.sortAsc; - break; - } - } - - if (e.metaKey && options.multiColumnSort) { - if (sortOpts) { - sortColumns.splice(i, 1); - } - } - else { - if ((!e.shiftKey && !e.metaKey) || !options.multiColumnSort) { - sortColumns = []; - } - - if (!sortOpts) { - sortOpts = { columnId: column.id, sortAsc: column.defaultSortAsc }; - sortColumns.push(sortOpts); - } else if (sortColumns.length == 0) { - sortColumns.push(sortOpts); - } - } - - setSortColumns(sortColumns); - - if (!options.multiColumnSort) { - trigger(self.onSort, { - multiColumnSort: false, - sortCol: column, - sortAsc: sortOpts.sortAsc}, e); - } else { - trigger(self.onSort, { - multiColumnSort: true, - sortCols: $.map(sortColumns, function(col) { - return {sortCol: columns[getColumnIndex(col.columnId)], sortAsc: col.sortAsc }; - })}, e); - } - } - }); - } - - function setupColumnReorder() { - $headers.filter(":ui-sortable").sortable("destroy"); - $headers.sortable({ - containment: "parent", - distance: 3, - axis: "x", - cursor: "default", - tolerance: "intersection", - helper: "clone", - placeholder: "slick-sortable-placeholder ui-state-default slick-header-column", - start: function (e, ui) { - ui.placeholder.width(ui.helper.outerWidth() - headerColumnWidthDiff); - $(ui.helper).addClass("slick-header-column-active"); - }, - beforeStop: function (e, ui) { - $(ui.helper).removeClass("slick-header-column-active"); - }, - stop: function (e) { - if (!getEditorLock().commitCurrentEdit()) { - $(this).sortable("cancel"); - return; - } - - var reorderedIds = $headers.sortable("toArray"); - var reorderedColumns = []; - for (var i = 0; i < reorderedIds.length; i++) { - reorderedColumns.push(columns[getColumnIndex(reorderedIds[i].replace(uid, ""))]); - } - setColumns(reorderedColumns); - - trigger(self.onColumnsReordered, {}); - e.stopPropagation(); - setupColumnResize(); - } - }); - } - - function setupColumnResize() { - var $col, j, c, pageX, columnElements, minPageX, maxPageX, firstResizable, lastResizable; - columnElements = $headers.children(); - columnElements.find(".slick-resizable-handle").remove(); - columnElements.each(function (i, e) { - if (columns[i].resizable) { - if (firstResizable === undefined) { - firstResizable = i; - } - lastResizable = i; - } - }); - if (firstResizable === undefined) { - return; - } - columnElements.each(function (i, e) { - if (i < firstResizable || (options.forceFitColumns && i >= lastResizable)) { - return; - } - $col = $(e); - $("
") - .appendTo(e) - .bind("dragstart", function (e, dd) { - if (!getEditorLock().commitCurrentEdit()) { - return false; - } - pageX = e.pageX; - $(this).parent().addClass("slick-header-column-active"); - var shrinkLeewayOnRight = null, stretchLeewayOnRight = null; - // lock each column's width option to current width - columnElements.each(function (i, e) { - columns[i].previousWidth = $(e).outerWidth(); - }); - if (options.forceFitColumns) { - shrinkLeewayOnRight = 0; - stretchLeewayOnRight = 0; - // colums on right affect maxPageX/minPageX - for (j = i + 1; j < columnElements.length; j++) { - c = columns[j]; - if (c.resizable) { - if (stretchLeewayOnRight !== null) { - if (c.maxWidth) { - stretchLeewayOnRight += c.maxWidth - c.previousWidth; - } else { - stretchLeewayOnRight = null; - } - } - shrinkLeewayOnRight += c.previousWidth - Math.max(c.minWidth || 0, absoluteColumnMinWidth); - } - } - } - var shrinkLeewayOnLeft = 0, stretchLeewayOnLeft = 0; - for (j = 0; j <= i; j++) { - // columns on left only affect minPageX - c = columns[j]; - if (c.resizable) { - if (stretchLeewayOnLeft !== null) { - if (c.maxWidth) { - stretchLeewayOnLeft += c.maxWidth - c.previousWidth; - } else { - stretchLeewayOnLeft = null; - } - } - shrinkLeewayOnLeft += c.previousWidth - Math.max(c.minWidth || 0, absoluteColumnMinWidth); - } - } - if (shrinkLeewayOnRight === null) { - shrinkLeewayOnRight = 100000; - } - if (shrinkLeewayOnLeft === null) { - shrinkLeewayOnLeft = 100000; - } - if (stretchLeewayOnRight === null) { - stretchLeewayOnRight = 100000; - } - if (stretchLeewayOnLeft === null) { - stretchLeewayOnLeft = 100000; - } - maxPageX = pageX + Math.min(shrinkLeewayOnRight, stretchLeewayOnLeft); - minPageX = pageX - Math.min(shrinkLeewayOnLeft, stretchLeewayOnRight); - }) - .bind("drag", function (e, dd) { - var actualMinWidth, d = Math.min(maxPageX, Math.max(minPageX, e.pageX)) - pageX, x; - if (d < 0) { // shrink column - x = d; - for (j = i; j >= 0; j--) { - c = columns[j]; - if (c.resizable) { - actualMinWidth = Math.max(c.minWidth || 0, absoluteColumnMinWidth); - if (x && c.previousWidth + x < actualMinWidth) { - x += c.previousWidth - actualMinWidth; - c.width = actualMinWidth; - } else { - c.width = c.previousWidth + x; - x = 0; - } - } - } - - if (options.forceFitColumns) { - x = -d; - for (j = i + 1; j < columnElements.length; j++) { - c = columns[j]; - if (c.resizable) { - if (x && c.maxWidth && (c.maxWidth - c.previousWidth < x)) { - x -= c.maxWidth - c.previousWidth; - c.width = c.maxWidth; - } else { - c.width = c.previousWidth + x; - x = 0; - } - } - } - } - } else { // stretch column - x = d; - for (j = i; j >= 0; j--) { - c = columns[j]; - if (c.resizable) { - if (x && c.maxWidth && (c.maxWidth - c.previousWidth < x)) { - x -= c.maxWidth - c.previousWidth; - c.width = c.maxWidth; - } else { - c.width = c.previousWidth + x; - x = 0; - } - } - } - - if (options.forceFitColumns) { - x = -d; - for (j = i + 1; j < columnElements.length; j++) { - c = columns[j]; - if (c.resizable) { - actualMinWidth = Math.max(c.minWidth || 0, absoluteColumnMinWidth); - if (x && c.previousWidth + x < actualMinWidth) { - x += c.previousWidth - actualMinWidth; - c.width = actualMinWidth; - } else { - c.width = c.previousWidth + x; - x = 0; - } - } - } - } - } - applyColumnHeaderWidths(); - if (options.syncColumnCellResize) { - applyColumnWidths(); - } - }) - .bind("dragend", function (e, dd) { - var newWidth; - $(this).parent().removeClass("slick-header-column-active"); - for (j = 0; j < columnElements.length; j++) { - c = columns[j]; - newWidth = $(columnElements[j]).outerWidth(); - - if (c.previousWidth !== newWidth && c.rerenderOnResize) { - invalidateAllRows(); - } - } - updateCanvasWidth(true); - render(); - trigger(self.onColumnsResized, {}); - }); - }); - } - - function getVBoxDelta($el) { - var p = ["borderTopWidth", "borderBottomWidth", "paddingTop", "paddingBottom"]; - var delta = 0; - $.each(p, function (n, val) { - delta += parseFloat($el.css(val)) || 0; - }); - return delta; - } - - function measureCellPaddingAndBorder() { - var el; - var h = ["borderLeftWidth", "borderRightWidth", "paddingLeft", "paddingRight"]; - var v = ["borderTopWidth", "borderBottomWidth", "paddingTop", "paddingBottom"]; - - el = $("").appendTo($headers); - headerColumnWidthDiff = headerColumnHeightDiff = 0; - if (el.css("box-sizing") != "border-box" && el.css("-moz-box-sizing") != "border-box" && el.css("-webkit-box-sizing") != "border-box") { - $.each(h, function (n, val) { - headerColumnWidthDiff += parseFloat(el.css(val)) || 0; - }); - $.each(v, function (n, val) { - headerColumnHeightDiff += parseFloat(el.css(val)) || 0; - }); - } - el.remove(); - - var r = $("
").appendTo($canvas); - el = $("").appendTo(r); - cellWidthDiff = cellHeightDiff = 0; - if (el.css("box-sizing") != "border-box" && el.css("-moz-box-sizing") != "border-box" && el.css("-webkit-box-sizing") != "border-box") { - $.each(h, function (n, val) { - cellWidthDiff += parseFloat(el.css(val)) || 0; - }); - $.each(v, function (n, val) { - cellHeightDiff += parseFloat(el.css(val)) || 0; - }); - } - r.remove(); - - absoluteColumnMinWidth = Math.max(headerColumnWidthDiff, cellWidthDiff); - } - - function createCssRules() { - $style = $("
 '+(serie.label || String.fromCharCode(65+i))+'